├── .dockerignore ├── .github └── workflows │ ├── build_docker_image.yml │ └── xgo_ci.yml ├── .idea └── .name ├── Dockerfile ├── LICENSE ├── Plugin.md ├── README.MD ├── build └── build.sh ├── config └── config.yaml ├── const └── const.go ├── docker-entrypoint.sh ├── go.mod ├── go.sum ├── main.go ├── sample ├── config │ └── config.yaml ├── plugin │ └── 插件目录.txt └── sample │ └── config.yaml ├── server ├── controllers │ ├── admin.go │ ├── cdk.go │ ├── container.go │ ├── env.go │ ├── javascript.go │ ├── message.go │ ├── open.go │ ├── panel.go │ ├── setting.go │ └── system.go ├── logic │ ├── cdkLogic.go │ ├── containerLogic.go │ ├── envLogic.go │ ├── messageLogic.go │ ├── openLogic.go │ ├── panelLogic.go │ ├── settingLogic.go │ ├── systemLogic.go │ └── userLogic.go ├── middleware │ ├── auth.go │ ├── cors.go │ └── limiter.go ├── model │ ├── cdkAdmin.go │ ├── conAdmin.go │ ├── envAdmin.go │ ├── jsAdmin.go │ ├── jwtAdmin.go │ ├── msgAdmin.go │ ├── panelAdmin.go │ ├── settingAdmin.go │ ├── sysAdmin.go │ └── userAdmin.go ├── routes.go ├── settings │ └── settings.go └── sqlite │ ├── cdkSQL.go │ ├── containerSQL.go │ ├── envSQL.go │ ├── jwtSQL.go │ ├── messageSQL.go │ ├── openSQL.go │ ├── panelSQL.go │ ├── settingSQL.go │ ├── sqlite.go │ ├── tokenSQL.go │ └── userSQL.go ├── static ├── assets │ ├── css │ │ ├── 134.2b81fd4b.css │ │ ├── 177.8f8f9b74.css │ │ ├── 246.8f8f9b74.css │ │ ├── 255.c768f918.css │ │ ├── 362.8f8f9b74.css │ │ ├── 452.ed4ca03e.css │ │ ├── 534.8f8f9b74.css │ │ ├── 557.8f8f9b74.css │ │ ├── 647.8f8f9b74.css │ │ ├── 657.8f8f9b74.css │ │ ├── 923.8f8f9b74.css │ │ ├── 946.e1dedb28.css │ │ └── app.568720ed.css │ ├── favicon.ico │ ├── index.html │ └── js │ │ ├── 134.2b48875b.js │ │ ├── 177.8be1d85b.js │ │ ├── 246.de643a8a.js │ │ ├── 255.92ecce8c.js │ │ ├── 362.511d1d3b.js │ │ ├── 452.9478c9cf.js │ │ ├── 493.fdb76b15.js │ │ ├── 534.7d39fcda.js │ │ ├── 557.411ca019.js │ │ ├── 647.f886fe08.js │ │ ├── 657.62dae764.js │ │ ├── 923.1bf6923d.js │ │ ├── 946.c08d7801.js │ │ ├── app.b8bfd2f5.js │ │ └── chunk-vendors.d15cd006.js ├── bindata │ └── bindata.go └── build.bat └── tools ├── email └── email.go ├── goja ├── goja.go └── gojaFunction.go ├── jwt └── jwt.go ├── logger └── logger.go ├── md5 └── md5.go ├── panel └── panel.go ├── requests └── requests.go ├── response ├── code.go └── response.go ├── snowflake └── snowflake.go ├── timeTools └── timeTools.go └── validator └── validator.go /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | README.* 4 | .github 5 | .git -------------------------------------------------------------------------------- /.github/workflows/build_docker_image.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: 构建QLTtools镜像到Docker 4 | 5 | # Controls when the action will run. 6 | on: 7 | # 任意推送都会触发构建 8 | #push: 9 | # 定时调度构建 10 | #schedule: 11 | #- cron: "0 0 * * 1" 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | # 手动触发 15 | workflow_dispatch: 16 | inputs: 17 | logLevel: 18 | description: 'Log level' 19 | required: true 20 | default: 'warning' 21 | pushdocker: 22 | description: '镜像是否推送到docker仓库,不勾选不推送,默认启用' 23 | type: boolean 24 | required: true 25 | default: 'true' 26 | pushghcr: 27 | description: '镜像是否推送到GHCR仓库,不勾选不推送,默认启用' 28 | type: boolean 29 | required: true 30 | default: 'true' 31 | latests: 32 | description: '镜像是否启用latest标签,不勾选不启用,默认启用' 33 | type: boolean 34 | required: true 35 | default: 'true' 36 | tags: 37 | description: 'tags:请填写镜像版本号,如1.0' 38 | required: true 39 | default: '' 40 | 41 | jobs: 42 | buildx: 43 | runs-on: ubuntu-latest 44 | permissions: write-all 45 | 46 | steps: 47 | - 48 | name: Checkout 49 | uses: actions/checkout@v3 50 | - 51 | name: Set up QEMU 52 | uses: docker/setup-qemu-action@v2 53 | - 54 | name: Set up Docker Buildx 55 | id: buildx 56 | uses: docker/setup-buildx-action@v2 57 | - 58 | name: Available platforms 59 | run: echo ${{ steps.buildx.outputs.platforms }} 60 | - 61 | name: Login to DockerHub 62 | uses: docker/login-action@v2 63 | with: 64 | username: ${{ secrets.DOCKER_USERNAME }} # 去secrets中添加 65 | password: ${{ secrets.DOCKER_PASSWORD }} # 去secrets中添加 66 | - 67 | name: Login to GHCR 68 | uses: docker/login-action@v2 69 | with: 70 | registry: ghcr.io 71 | username: ${{ github.repository_owner }} 72 | password: ${{ secrets.GITHUB_TOKEN }} 73 | - 74 | name: image Name change 75 | id: read-docker-image-identifiers # 去secrets中添加DOCKER_IMAGENAME 镜像名称,自动字母大写转换小写。 76 | run: | 77 | echo DOCKER_IMAGENAME=$(echo ${{ secrets.DOCKER_IMAGENAME }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 78 | - 79 | name: Extract metadata (tags, labels) for Docker 80 | if: ${{ github.event.inputs.pushdocker == 'true' || github.event.inputs.pushghcr == 'true' }} 81 | id: meta # 处理并输出镜像tag版本号[docker/metadata-action@master] 82 | uses: docker/metadata-action@v4 83 | with: 84 | images: | 85 | name=${{ secrets.DOCKER_USERNAME }}/${{ env.DOCKER_IMAGENAME }},enable=${{ github.event.inputs.pushdocker == 'true' }} 86 | name=ghcr.io/${{ github.repository_owner }}/${{ env.DOCKER_IMAGENAME }},enable=${{ github.event.inputs.pushghcr == 'true' }} 87 | # generate Docker tags based on the following events/attributes # 推送到GitHub packages仓库,需要打开『 https://github.com/settings/tokens/new?scopes=write:packages 』创建一个 PAT,勾选 write:packages 权限,才可使用。 88 | # metadata 使用说明:https://github.com/docker/metadata-action 89 | # nightly, master, pr-2, 1.2.3, 1.2, 1 90 | tags: | 91 | type=raw,value=${{ github.event.inputs.tags }},priority=1000 92 | type=raw,value=latest,enable=${{ github.event.inputs.latests == 'true' }} 93 | - 94 | name: Build and push 95 | uses: docker/build-push-action@v3 96 | with: 97 | context: . # 工作目录 98 | file: ./Dockerfile #构建文件路径 99 | # 所需要的体系结构,可以在 Available platforms 步骤中获取所有的可用架构【前提:基础镜像必须要有对应的架构,否则无法构建】 100 | platforms: linux/amd64,linux/arm64/v8,linux/arm/v7 101 | #platforms可选架构: linux/amd64,linux/amd64/v2,linux/amd64/v3,linux/amd64/v4,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 102 | # 镜像推送 103 | push: ${{ github.event.inputs.pushdocker == 'true' || github.event.inputs.pushghcr == 'true' }} 104 | # 给清单打上多个tags标签 105 | tags: ${{ steps.meta.outputs.tags }} 106 | labels: ${{ steps.meta.outputs.labels }} 107 | -------------------------------------------------------------------------------- /.github/workflows/xgo_ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | #release: 5 | #types: [created] # 表示在创建新的 Release 时触发 6 | workflow_dispatch: 7 | inputs: 8 | logLevel: 9 | description: 'Log level' 10 | required: true 11 | default: 'warning' 12 | xgoversion: 13 | description: '自定义docker版xgo镜像地址和标签[保持默认值即可,谨慎填写],如:ghcr.io/grbhq/xgo:go-1.20.0' 14 | required: true 15 | default: 'techknowlogick/xgo:latest' 16 | pushartifact: 17 | description: '是否拆分上传二进制文件到artifact,不勾选不拆分上传,默认不拆分' 18 | type: boolean 19 | required: true 20 | default: 'false' 21 | pushrelease: 22 | description: '是否发布release,不勾选不发布,默认发布' 23 | type: boolean 24 | required: true 25 | default: 'true' 26 | forcedtag: 27 | description: 'tag标签重复时是否强制覆盖,勾选后自动覆盖,默认关闭' 28 | type: boolean 29 | required: true 30 | default: 'false' 31 | tag: 32 | description: '输入tag版本号,如1.0' 33 | required: true 34 | default: '' 35 | 36 | env: 37 | WORK_DIR: "QLTools" 38 | 39 | jobs: 40 | build: 41 | name: Build binary CI 42 | runs-on: ubuntu-latest 43 | outputs: 44 | file_lists: ${{ steps.filenames.outputs.file_lists }} 45 | 46 | permissions: write-all 47 | 48 | steps: 49 | - uses: actions/checkout@v3 50 | - name: Setup Go environment 51 | uses: actions/setup-go@v3 52 | with: 53 | cache: true 54 | go-version: '1.20' 55 | 56 | # 利用xgo交叉编译二进制文件 # 仓库地址:https://github.com/techknowlogick/xgo 57 | - name: Build go binary file 58 | run: | 59 | mkdir dist-$WORK_DIR && cd dist-$WORK_DIR 60 | docker pull ${{ github.event.inputs.xgoversion }} 61 | go install src.techknowlogick.com/xgo@latest 62 | xgo -ldflags '-s -w' -out $WORK_DIR --targets=linux/*,windows/386,windows/amd64,darwin/amd64,darwin/arm64 github.com/${{ github.repository }} 63 | cd .. && ls -al ./dist-$WORK_DIR 64 | 65 | # 打包上传二进制文件到artifact,默认90天有效期 66 | - name: Upload artifact 67 | uses: actions/upload-artifact@v3 68 | with: 69 | name: ${{ env.WORK_DIR }}-${{ github.run_number }} 70 | path: dist-${{ env.WORK_DIR }}/ 71 | 72 | # 检测输入的tag版本格式[如:v1.2.0;0.1;0.5.6;2.20.100]均可行 73 | - name: check tag_version 74 | if: ${{ github.event.inputs.pushrelease == 'true' }} 75 | id: checktag 76 | run: | 77 | if echo "${{ github.event.inputs.tag }}" | grep -P "^(?!v0+(\.0+)*$)v?\d+(\.\d+)+$"; then 78 | echo "The version ${{ github.event.inputs.tag }} is formatted correctly" 79 | echo "runnable=true" >> $GITHUB_OUTPUT 80 | else 81 | echo "The version ${{ github.event.inputs.tag }} is malformed" 82 | exit 0 83 | fi 84 | 85 | # 强制推送已存在的tag 86 | - name: forced tag 87 | if: ${{ github.event.inputs.pushrelease == 'true' && github.event.inputs.forcedtag == 'true' && steps.checktag.outputs.runnable == 'true' }} 88 | run: | 89 | git config --local user.name "github-actions[bot]" 90 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 91 | git tag -f ${{ github.event.inputs.tag }} 92 | git push origin -f ${{ github.event.inputs.tag }} 93 | 94 | # 上传二进制文件到release 95 | - name: Upload binaries to release 96 | if: ${{ github.event.inputs.pushrelease == 'true' && steps.checktag.outputs.runnable == 'true' }} 97 | uses: svenstaro/upload-release-action@v2 98 | with: 99 | repo_token: ${{ secrets.GITHUB_TOKEN }} 100 | file: dist-${{ env.WORK_DIR }}/* 101 | file_glob: true # 多文件上传 102 | tag: ${{ github.event.inputs.tag }} 103 | release_name: ${{ github.event.inputs.tag }} 104 | prerelease: false 105 | overwrite: true 106 | body: "- This is my release text" 107 | 108 | # 获取二进制文件列表 109 | - name: binary file lists 110 | id: filenames 111 | run: | 112 | folder_path="dist-$WORK_DIR" 113 | file_names=$(find "$folder_path" -type f -exec basename {} \; | sed 's/.*/"&"/' | paste -sd, - | sed -e '1s/^/\[/' -e '$s/$/\]/') 114 | echo "file_lists=$file_names" >> $GITHUB_OUTPUT 115 | 116 | artifact: 117 | needs: build 118 | runs-on: ubuntu-latest 119 | if: ${{ github.event.inputs.pushartifact == 'true' }} 120 | permissions: write-all 121 | strategy: 122 | fail-fast: true 123 | max-parallel: 6 124 | matrix: 125 | lists: ${{ fromJson(needs.build.outputs.file_lists) }} 126 | 127 | steps: 128 | - name: Checkout 129 | uses: actions/checkout@v3 130 | 131 | # 下载artifact中的文件到本地 132 | - name: download artifact 133 | uses: actions/download-artifact@v3 134 | with: 135 | name: ${{ env.WORK_DIR }}-${{ github.run_number }} 136 | path: dist-${{ env.WORK_DIR }}/ 137 | 138 | # 单个依次上传二进制文件到artifact,默认90天有效期 139 | - name: Upload artifact 140 | uses: actions/upload-artifact@v3 141 | with: 142 | name: ${{ matrix.lists }} 143 | path: dist-${{ env.WORK_DIR }}/${{ matrix.lists }} 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | QLTools -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.18.2 AS builder 2 | 3 | ARG TARGETARCH 4 | 5 | ENV GO111MODULE=on \ 6 | CGO_ENABLED=1 \ 7 | GOOS=linux \ 8 | GOPROXY=https://goproxy.cn,direct 9 | 10 | WORKDIR /usr/src/QLTools 11 | 12 | # 安装项目必要环境 13 | RUN \ 14 | #sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ 15 | apk add --no-cache --update go go-bindata g++ ca-certificates tzdata 16 | 17 | COPY ./go.mod ./ 18 | COPY ./go.sum ./ 19 | RUN go mod download 20 | 21 | COPY . . 22 | 23 | # 打包项目文件 24 | RUN \ 25 | #go-bindata -o=bindata/bindata.go -pkg=bindata ./assets/... && \ 26 | go build -ldflags '-linkmode external -s -w -extldflags "-static"' -o QLTools-linux-$TARGETARCH 27 | 28 | 29 | # FROM alpine:3.15 30 | FROM ubuntu:22.10 31 | 32 | MAINTAINER QLTools "nuanxinqing@gmail.com" 33 | 34 | ARG TARGETARCH 35 | ENV TARGET_ARCH=$TARGETARCH 36 | 37 | WORKDIR /QLTools 38 | 39 | COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 40 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 41 | COPY --from=builder /usr/src/QLTools/QLTools-linux-$TARGETARCH /usr/src/QLTools/docker-entrypoint.sh /usr/src/QLTools/sample ./ 42 | 43 | EXPOSE 15000 44 | 45 | ENTRYPOINT ["sh", "docker-entrypoint.sh"] 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Plugin.md: -------------------------------------------------------------------------------- 1 | # 插件开发模板 2 | 3 | ```javascript 4 | // [name:Cookie检测(demo 插件开发演示)] 5 | 6 | // 第一行为插件名称, 在后台显示的使用会用到 7 | 8 | // 返回数据格式 9 | // return { 10 | // // 代表是否允许通过 11 | // "bool": true, 12 | // // 处理后的变量 13 | // "env": env 14 | // } 15 | 16 | // 必须以main为函数名, env为传入变量 17 | function main(env) { 18 | let result = request({ 19 | "method": "get", 20 | "url": "https://plogin.m.jd.com/cgi-bin/ml/islogin", 21 | "headers": { 22 | "Cookie": env, 23 | "User-Agent": "jdapp;iPhone;9.4.4;14.3;network/4g;Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148;supportJDSHWK/1", 24 | "Referer": "https://h5.m.jd.com/" 25 | }, 26 | "dataType": "json", 27 | "timeout": 5 * 1000 28 | }) 29 | 30 | if (result) { 31 | // 判断是否过期 32 | if (result["islogin"] === "1"){ 33 | // Cookie有效 34 | return { 35 | "bool": true, 36 | "env": env 37 | } 38 | } else { 39 | // Cookie无效 40 | return { 41 | "bool": false, 42 | "env": "Cookie已失效" 43 | } 44 | } 45 | } else { 46 | return { 47 | "bool": false, 48 | "env": "请求失败" 49 | } 50 | } 51 | } 52 | ``` 53 | 54 | ## 封装可用方法 55 | 56 | ### *Request* 57 | 58 | ``` 59 | let result = request({ 60 | // 请求方式(默认get) 61 | "method": "get", 62 | // 请求地址 63 | "url": "https://me-api.jd.com/user_new/info/GetJDUserInfoUnion", 64 | // 数据类型(返回数据如果是JSON那么就需要指定为json,否则默认为location) 65 | "dataType": "", 66 | // 请求头 67 | "headers": { 68 | "Cookie": env, 69 | "User-Agent": "jdapp;iPhone;9.4.4;14.3;network/4g;Mozilla/5.0 (iPhone; CPU iPhone OS 14_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148;supportJDSHWK/1", 70 | "Referer": "https://h5.m.jd.com/" 71 | }, 72 | // 请求体(body为json请求体, 二选一即可) 73 | "body": {}, 74 | "formdata": {}, 75 | // 自定义超时(单位:纳秒)(5 * 1000 = 5秒) 76 | "timeout": 5 * 1000 77 | }) 78 | ``` 79 | 80 | ## *ReFind*方法 81 | 82 | ``` 83 | // ReFind(正则表达式, 待匹配数据) 返回:匹配结果列表(数组)(string) 84 | let result = ReFind("pt_pin=(.*?);", "pt_pin=xxx") 85 | ``` 86 | 87 | ### *consolo方法* 88 | 89 | ``` 90 | // 填写打印信息即可(string) 91 | console.info() 92 | console.debug() 93 | console.warn() 94 | console.error() 95 | console.log() 96 | ``` 97 | 98 | ### *Replace方法* 99 | 100 | ``` 101 | // 参数说明:替换文本中的关键词 102 | // s(string):原始字符串 103 | // old(string):需要替换的内容 104 | // new(string):替换后的内容 105 | // count(int):需要替换的数量,不填写默认为替换第一个 106 | let result = Replace(s, old, new, count) 107 | ``` 108 | 109 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 | Img 5 | 6 |

7 | 8 |

青龙Tools

9 |

查看演示图

10 |

V2版本正在开发中: 点击前往

11 | 12 | ## 🍭功能介绍 13 | - 理论支持青龙全版本 14 | - 自定义变量名称 & 数量 15 | - 支持多容器上传管控 16 | - 容器独享变量限额 17 | - 自选服务器和变量组上传 18 | - 自助识别添加面板已存在变量信息(未) 19 | - 支持变量黑名单 & IP提交次数限制 20 | - 支持容器相互迁移 & 复制 & 备份 & 恢复 21 | - 插件模式(可将自行编写插件绑定到变量上使用) 22 | - 程序热更新(快速更新版本) 23 | - 自定义主题编写,前后端分离支持 24 | 25 | ## 🍳技术栈 26 | - 语言:Golang 27 | - Web框架:Gin框架 28 | - 配置文件:Viper库 29 | - 日志:Zap库 30 | - 数据库:GORM - SQLite 31 | - API文档:https://console-docs.apipost.cn/preview/0fdb4c815ed24ab2/e2057f0e0b8dc545 32 | - 文档密码:609889 33 | 34 | ## 🧸配置文件 35 | ```yaml 36 | app: 37 | # 运行模式:生产环境留空(开发模式:debug) 38 | mode: "" 39 | # 运行端口 40 | port: 15000 41 | ``` 42 | 43 | ## 🔍安装教程 44 | 默认后台信息【第一次运行需要先注册账户,没有默认账户】 45 | - 登录地址:IP或域名:15000/#/login 46 | - 注册地址:IP或域名:15000/#/register 47 | - 后台地址:IP或域名:15000/#/admin 48 | 49 | Tips: 50 | - 测试版不提供Docker镜像 51 | - 删除config目录里面的app.db文件就相当于重装青龙Tools 52 | 53 | 安装方式一:Supervisord & PM2 & nohup 启动守护 54 | ```shell 55 | # 创建目录并进入 56 | mkdir QLTools && cd QLTools 57 | 58 | # 给予权限 59 | chmod 755 程序名称 60 | 61 | # 启动程序排查启动错误 62 | ./程序名称 63 | 64 | # 程序无误后点击下方教程查看程序后台守护教程 65 | ``` 66 | 67 | 教程地址:[青龙Tools的后台进程守护教程(附反代域名)](https://6b7.org/460.html) 68 | 69 | 安装方式二、Docker启动 70 | ```shell 71 | # 创建QLTools目录并进入 72 | mkdir qltools && cd qltools 73 | 74 | # Docker版本提供架构:amd64、arm64、arm-7 75 | docker run --restart=always -itd --name QLTools -v $PWD/config:/QLTools/config -v $PWD/plugin:/QLTools/plugin -p 15000:15000 nuanxinqing123/qltools:latest 76 | 77 | # 更新步骤:后台点击更新,稍等5分钟左右。手动重启容器即可完成更新(如果更新失败,请删除容器和镜像,然后重新拉取镜像启动) 78 | # 重启命令:docker restart QLTools 79 | ``` 80 | 81 | ## 🎯开发计划 82 | 83 | 开发计划 & 进度:[点击查看](https://web.banlikanban.com/kanban/626f9b4c6ade1220282ac551) 84 | 85 | 【联系方式】 86 | 87 | TG:[https://t.me/Nuanxinqing](https://t.me/Nuanxinqing) 88 | 89 | Email:nuanxinqing@gmail.com 90 | 91 | 【交流群】 92 | 93 | 青龙Tools频道:[https://t.me/qltool](https://t.me/qltool) 94 | 95 | 青龙Tools吹水群:[https://t.me/qltools](https://t.me/qltools) 96 | 97 | ## 🧩当前版本日志 98 | 99 | - 修复 变量上传的稳定性 100 | - 修复 插件创建流程错误 101 | - 新增 合并模式换行符分割 102 | - 优化 前端描述文档修改 103 | 104 | ## 📔自行构建步骤 105 | ```shell 106 | # 项目开发环境:Golang 1.19 107 | # Clone 项目并进入项目目录 108 | git clone https://github.com/nuanxinqing123/QLTools.git && cd QLTools 109 | 110 | # 更新项目依赖 111 | go mod tidy 112 | 113 | # 打包 114 | go build 115 | 116 | ------------------------------------------------------------- 117 | 118 | # XGO 批量打包 119 | xgo -out QLTools --targets=windows/*,linux/* 青龙Tools代码路径 120 | # 举例 121 | xgo -out QLTools --targets=windows/*,linux/* /home/Go/QLTools 122 | 123 | ------------------------------------------------------------- 124 | 125 | # 自行编写的构建前端静态文件(需要安装go-bindata) 126 | go-bindata -o=bindata/bindata.go -pkg=bindata ./assets/... 127 | 128 | # 前端自行修改教程:https://6b7.org/518.html 129 | ``` 130 | 131 | ## Star History 132 | 133 | [![Star History Chart](https://api.star-history.com/svg?repos=nuanxinqing123/QLTools&type=Date)](https://star-history.com/#nuanxinqing123/QLTools&Date) 134 | 135 | ## 📷演示图 136 | 137 | ![QQ截图20220511154438.png](https://pic.6b7.xyz/2022/05/11/25a5e41170f5f.png) 138 | 139 | ![QQ截图20220511154454.png](https://pic.6b7.xyz/2022/05/11/3f13f15d25b46.png) 140 | 141 | ![QQ截图20220511154818.png](https://pic.6b7.xyz/2022/05/11/e41ea41542307.png) 142 | 143 | ![QQ截图20220511154933.png](https://pic.6b7.xyz/2022/05/11/40f36ef85f79d.png) 144 | 145 | ![QQ截图20220511154947.png](https://pic.6b7.xyz/2022/05/11/347a5fd9b12f2.png) 146 | 147 | ![QQ截图20220511155004.png](https://pic.6b7.xyz/2022/05/11/3c3c339fa3b82.png) 148 | 149 | ![QQ截图20220511155021.png](https://pic.6b7.xyz/2022/05/11/4fe5dab516d93.png) 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /build/build.sh: -------------------------------------------------------------------------------- 1 | xgo -out QLTools --targets=windows/*,linux/* ../ 2 | upx QLTools-* -------------------------------------------------------------------------------- /config/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | # 运行模式:生产环境留空 3 | mode: "debug" 4 | # 运行端口 5 | port: 15000 -------------------------------------------------------------------------------- /const/const.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/27 18:46 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : const.go 6 | 7 | package _const 8 | 9 | // Version 全局变量 10 | const ( 11 | Version = "2.3" 12 | ) 13 | -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ ! -s /QLTools/config/config.yaml ]; then 5 | echo "检测到config配置目录下不存在config.yaml,从示例文件复制一份用于初始化...\n" 6 | cp -fv /QLTools/sample/config.yaml /QLTools/config/config.yaml 7 | fi 8 | 9 | if [ -s /QLTools/config/config.yaml ]; then 10 | echo "检测到config配置目录下存在config.yaml,即将启动...\n" 11 | 12 | ./QLTools-linux-${TARGET_ARCH} 13 | 14 | fi 15 | 16 | exec "$@" 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module QLPanelTools 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/beego/beego/v2 v2.0.4 7 | github.com/bwmarrin/snowflake v0.3.0 8 | github.com/dop251/goja v0.0.0-20220501172647-e1eca0b61fa9 9 | github.com/elazarl/go-bindata-assetfs v1.0.1 10 | github.com/fsnotify/fsnotify v1.5.4 11 | github.com/gin-gonic/gin v1.9.1 12 | github.com/go-playground/locales v0.14.1 13 | github.com/go-playground/universal-translator v0.18.1 14 | github.com/go-playground/validator/v10 v10.14.0 15 | github.com/golang-jwt/jwt v3.2.2+incompatible 16 | github.com/juju/ratelimit v1.0.1 17 | github.com/natefinch/lumberjack v2.0.0+incompatible 18 | github.com/segmentio/ksuid v1.0.4 19 | github.com/spf13/viper v1.12.0 20 | github.com/staktrace/go-update v0.0.0-20210525161054-fc019945f9a2 21 | go.uber.org/zap v1.21.0 22 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df 23 | gopkg.in/yaml.v3 v3.0.1 24 | gorm.io/driver/sqlite v1.3.1 25 | gorm.io/gorm v1.23.1 26 | ) 27 | 28 | require ( 29 | github.com/BurntSushi/toml v1.1.0 // indirect 30 | github.com/bytedance/sonic v1.9.1 // indirect 31 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 32 | github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect 33 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 34 | github.com/gin-contrib/sse v0.1.0 // indirect 35 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 36 | github.com/goccy/go-json v0.10.2 // indirect 37 | github.com/hashicorp/hcl v1.0.0 // indirect 38 | github.com/jinzhu/inflection v1.0.0 // indirect 39 | github.com/jinzhu/now v1.1.4 // indirect 40 | github.com/json-iterator/go v1.1.12 // indirect 41 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 42 | github.com/leodido/go-urn v1.2.4 // indirect 43 | github.com/magiconair/properties v1.8.6 // indirect 44 | github.com/mattn/go-isatty v0.0.19 // indirect 45 | github.com/mattn/go-sqlite3 v1.14.9 // indirect 46 | github.com/mitchellh/mapstructure v1.5.0 // indirect 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 48 | github.com/modern-go/reflect2 v1.0.2 // indirect 49 | github.com/pelletier/go-toml v1.9.5 // indirect 50 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 51 | github.com/pkg/errors v0.9.1 // indirect 52 | github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect 53 | github.com/spf13/afero v1.8.2 // indirect 54 | github.com/spf13/cast v1.5.0 // indirect 55 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 56 | github.com/spf13/pflag v1.0.5 // indirect 57 | github.com/subosito/gotenv v1.3.0 // indirect 58 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 59 | github.com/ugorji/go/codec v1.2.11 // indirect 60 | go.uber.org/atomic v1.7.0 // indirect 61 | go.uber.org/multierr v1.6.0 // indirect 62 | golang.org/x/arch v0.3.0 // indirect 63 | golang.org/x/crypto v0.21.0 // indirect 64 | golang.org/x/net v0.23.0 // indirect 65 | golang.org/x/sys v0.18.0 // indirect 66 | golang.org/x/text v0.14.0 // indirect 67 | google.golang.org/protobuf v1.33.0 // indirect 68 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect 69 | gopkg.in/ini.v1 v1.66.4 // indirect 70 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 71 | gopkg.in/yaml.v2 v2.4.0 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 12:46 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : main.go 6 | 7 | package main 8 | 9 | import ( 10 | _const "QLPanelTools/const" 11 | "QLPanelTools/server" 12 | "QLPanelTools/server/settings" 13 | "QLPanelTools/server/sqlite" 14 | "QLPanelTools/tools/logger" 15 | "QLPanelTools/tools/snowflake" 16 | "QLPanelTools/tools/validator" 17 | "context" 18 | "fmt" 19 | "log" 20 | "net/http" 21 | "os" 22 | "os/signal" 23 | "path/filepath" 24 | "strconv" 25 | "syscall" 26 | "time" 27 | 28 | "github.com/gin-gonic/gin" 29 | "github.com/spf13/viper" 30 | "go.uber.org/zap" 31 | "gopkg.in/yaml.v3" 32 | ) 33 | 34 | func main() { 35 | // 判断注册默认配置文件 36 | bol := IFConfig("config/config.yaml") 37 | if bol != true { 38 | fmt.Println("自动生成配置文件失败, 请按照仓库的配置文件模板手动在config目录下创建 config.yaml 配置文件") 39 | return 40 | } 41 | // 判断注册插件文件夹 42 | bol = IFPlugin() 43 | if bol != true { 44 | fmt.Println("自动创建插件文件夹失败, 请手动在程序根目录创建 plugin 文件夹") 45 | return 46 | } 47 | 48 | // 加载配置文件 49 | if err := settings.Init(); err != nil { 50 | fmt.Printf("viper init failed, err:%v\n", err) 51 | return 52 | } 53 | 54 | // 初始化日志 55 | if err := logger.Init(); err != nil { 56 | fmt.Printf("Logger init failed, err:%v\n", err) 57 | return 58 | } 59 | defer func(l *zap.Logger) { 60 | _ = l.Sync() 61 | }(zap.L()) 62 | zap.L().Debug("Logger success init ...") 63 | 64 | // 初始化数据库 65 | sqlite.Init() 66 | sqlite.InitWebSettings() 67 | zap.L().Debug("SQLite success init ...") 68 | 69 | // 初始化翻译器 70 | if err := validator.InitTrans("zh"); err != nil { 71 | fmt.Printf("validator init failed, err:%v\n", err) 72 | return 73 | } 74 | zap.L().Debug("Validator success init ...") 75 | 76 | // 注册雪花ID算法 77 | if err := snowflake.Init(); err != nil { 78 | fmt.Printf("snowflake init failed, err:%v\n", err) 79 | return 80 | } 81 | zap.L().Debug("Snowflake success init ...") 82 | 83 | // 运行模式 84 | if viper.GetString("app.mode") == "debug" { 85 | gin.SetMode(gin.DebugMode) 86 | } else { 87 | gin.SetMode(gin.ReleaseMode) 88 | } 89 | 90 | // 注册路由 91 | r := routes.Setup() 92 | 93 | // 启动服务 94 | srv := &http.Server{ 95 | Addr: fmt.Sprintf(":%d", viper.GetInt("app.port")), 96 | Handler: r, 97 | } 98 | 99 | fmt.Println(` _______ _ _________ _______ _______ _ _______ 100 | ( ___ )( \ \__ __/( ___ )( ___ )( \ ( ____ \ 101 | | ( ) || ( ) ( | ( ) || ( ) || ( | ( \/ 102 | | | | || | | | | | | || | | || | | (_____ 103 | | | | || | | | | | | || | | || | (_____ ) 104 | | | /\| || | | | | | | || | | || | ) | 105 | | (_\ \ || (____/\| | | (___) || (___) || (____/Y\____) | 106 | (____\/_)(_______/)_( (_______)(_______)(_______|_______)`) 107 | fmt.Println(" ") 108 | if viper.GetString("app.mode") == "debug" { 109 | fmt.Println("运行模式:Debug模式") 110 | } else { 111 | fmt.Println("运行模式:Release模式") 112 | } 113 | fmt.Println("系统版本:" + _const.Version) 114 | fmt.Println("登录地址:IP或域名:" + strconv.Itoa(viper.GetInt("app.port")) + "/#/login") 115 | fmt.Println("注册地址:IP或域名:" + strconv.Itoa(viper.GetInt("app.port")) + "/#/register") 116 | fmt.Println("后台地址:IP或域名:" + strconv.Itoa(viper.GetInt("app.port")) + "/#/admin") 117 | fmt.Println("监听端口:" + strconv.Itoa(viper.GetInt("app.port"))) 118 | fmt.Println(" ") 119 | zap.L().Info("监听端口:" + strconv.Itoa(viper.GetInt("app.port"))) 120 | 121 | // 启动 122 | go func() { 123 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 124 | log.Fatalf("Listten: %s\n", err) 125 | } 126 | }() 127 | 128 | // 等待终端信号来优雅关闭服务器,为关闭服务器设置5秒超时 129 | quit := make(chan os.Signal, 1) // 创建一个接受信号的通道 130 | 131 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // 此处不会阻塞 132 | <-quit // 阻塞此处,当接受到上述两种信号时,才继续往下执行 133 | zap.L().Info("Service ready to shut down") 134 | 135 | // 创建五秒超时的Context 136 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 137 | defer cancel() 138 | // 五秒内优雅关闭服务(将未处理完成的请求处理完再关闭服务),超过五秒就超时退出 139 | if err := srv.Shutdown(ctx); err != nil { 140 | zap.L().Fatal("Service timed out has been shut down:", zap.Error(err)) 141 | } 142 | 143 | zap.L().Info("Service has been shut down") 144 | } 145 | 146 | type Config struct { 147 | App App `yaml:"app"` 148 | } 149 | 150 | type App struct { 151 | Mode string `yaml:"mode"` 152 | Port int `yaml:"port"` 153 | } 154 | 155 | // IFConfig 判断并自动生成启动配置文件 156 | func IFConfig(src string) bool { 157 | // 检测是否存在配置文件 158 | file, err := os.Stat(src) 159 | if err != nil { 160 | err = os.Mkdir("config", 0777) 161 | if err != nil { 162 | fmt.Printf("Create Config Dir Error: %s", err) 163 | return false 164 | } 165 | err = os.Chmod("config", 0777) 166 | if err != nil { 167 | fmt.Printf("Chmod Config Dir Error: %s", err) 168 | return false 169 | } 170 | _, err = os.Create(src) 171 | if err != nil { 172 | fmt.Printf("Create Config File Error: %s", err) 173 | return false 174 | } 175 | 176 | // 需要生成默认 177 | cfg := &Config{ 178 | App: App{ 179 | Mode: "", 180 | Port: 15000, 181 | }, 182 | } 183 | data, err := yaml.Marshal(cfg) 184 | if err != nil { 185 | fmt.Printf("Marshal Config Error: %s", err) 186 | return false 187 | } 188 | 189 | // 写入默认配置 190 | err = os.WriteFile(src, data, 0777) 191 | if err != nil { 192 | fmt.Printf("WriteFile Config Error: %s", err) 193 | return false 194 | } 195 | 196 | return true 197 | } 198 | if file.IsDir() { 199 | return false 200 | } else { 201 | return true 202 | } 203 | } 204 | 205 | // IFPlugin 判断并自动创建插件文件夹 206 | func IFPlugin() bool { 207 | ExecPath, err := filepath.Abs(filepath.Dir(os.Args[0])) 208 | if err != nil { 209 | fmt.Println(err) 210 | return false 211 | } 212 | _, err = os.Stat(ExecPath + "/plugin") 213 | if err != nil { 214 | err = os.Mkdir("plugin", 0777) 215 | if err != nil { 216 | if err.Error() == "mkdir plugin: file exists" { 217 | return true 218 | } else { 219 | fmt.Println(err) 220 | return false 221 | } 222 | 223 | } 224 | _, err2 := os.Stat(ExecPath + "/plugin") 225 | if err2 != nil { 226 | fmt.Println(err) 227 | return false 228 | } 229 | } 230 | 231 | return true 232 | } 233 | -------------------------------------------------------------------------------- /sample/config/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | # 运行模式:生产环境留空 3 | mode: "" 4 | # 运行端口 5 | port: 15000 -------------------------------------------------------------------------------- /sample/plugin/插件目录.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuanxinqing123/QLTools/dbb09200d5dc23e1f5e896af0723e26b1535cd90/sample/plugin/插件目录.txt -------------------------------------------------------------------------------- /sample/sample/config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | # 运行模式:生产环境留空 3 | mode: "" 4 | # 运行端口 5 | port: 15000 -------------------------------------------------------------------------------- /server/controllers/admin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 15:06 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : admin.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | "QLPanelTools/tools/panel" 13 | res "QLPanelTools/tools/response" 14 | val "QLPanelTools/tools/validator" 15 | "github.com/gin-gonic/gin" 16 | "github.com/go-playground/validator/v10" 17 | "go.uber.org/zap" 18 | "net/http" 19 | ) 20 | 21 | func AdminTest(c *gin.Context) { 22 | c.JSON(http.StatusOK, gin.H{ 23 | "Code": 200, 24 | }) 25 | } 26 | 27 | // GetPanelToken 面板连接测试 28 | func GetPanelToken(c *gin.Context) { 29 | // 获取参数 30 | p := new(model.PanelData) 31 | if err := c.ShouldBindJSON(&p); err != nil { 32 | // 参数校验 33 | zap.L().Error("SignUpHandle with invalid param", zap.Error(err)) 34 | 35 | // 判断err是不是validator.ValidationErrors类型 36 | errs, ok := err.(validator.ValidationErrors) 37 | if !ok { 38 | res.ResError(c, res.CodeInvalidParam) 39 | return 40 | } 41 | 42 | // 翻译错误 43 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 44 | return 45 | } 46 | 47 | // 业务处理 48 | resCode, token := panel.TestGetPanelToken(p.URL, p.ID, p.Secret) 49 | if resCode == res.CodeServerBusy { 50 | // 内部服务错误 51 | res.ResError(c, res.CodeServerBusy) 52 | return 53 | } 54 | 55 | if token.Code != 200 { 56 | // 授权错误 57 | res.ResErrorWithMsg(c, res.CodeDataError, "client_id或client_secret有误") 58 | return 59 | } 60 | 61 | res.ResSuccess(c, "面板连接测试成功") 62 | return 63 | } 64 | 65 | // ReAdminPwd 修改管理员密码 66 | func ReAdminPwd(c *gin.Context) { 67 | // 获取参数 68 | p := new(model.ReAdminPwd) 69 | if err := c.ShouldBindJSON(&p); err != nil { 70 | // 参数校验 71 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 72 | 73 | // 判断err是不是validator.ValidationErrors类型 74 | errs, ok := err.(validator.ValidationErrors) 75 | if !ok { 76 | res.ResError(c, res.CodeInvalidParam) 77 | return 78 | } 79 | 80 | // 翻译错误 81 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 82 | return 83 | } 84 | 85 | // 处理业务 86 | bol, resCode := logic.RePwd(p) 87 | switch resCode { 88 | case res.CodeOldPassWordError: 89 | // 旧密码错误 90 | res.ResError(c, res.CodeOldPassWordError) 91 | case res.CodeServerBusy: 92 | // 保存修改错误 93 | res.ResError(c, res.CodeServerBusy) 94 | case res.CodeSuccess: 95 | // 修改成功 96 | res.ResSuccess(c, bol) 97 | } 98 | } 99 | 100 | // GetIPInfo 获取近十次登录 101 | func GetIPInfo(c *gin.Context) { 102 | // 处理业务 103 | bol, resCode := logic.GetIPInfo() 104 | switch resCode { 105 | case res.CodeSuccess: 106 | // 获取成功 107 | res.ResSuccess(c, bol) 108 | } 109 | } 110 | 111 | // GetAdminInfo 获取管理员信息 112 | func GetAdminInfo(c *gin.Context) { 113 | info, resCode := logic.GetAdminInfo() 114 | switch resCode { 115 | case res.CodeSuccess: 116 | // 修改成功 117 | res.ResSuccess(c, info) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /server/controllers/cdk.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/12 14:09 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : cdk.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | res "QLPanelTools/tools/response" 13 | val "QLPanelTools/tools/validator" 14 | "fmt" 15 | 16 | "github.com/gin-gonic/gin" 17 | "github.com/go-playground/validator/v10" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | // GetDivisionCDKData CDK:以20条数据分割 22 | func GetDivisionCDKData(c *gin.Context) { 23 | // 获取查询页码 24 | page := c.Query("page") 25 | resCode, data := logic.GetDivisionCDKData(page) 26 | 27 | switch resCode { 28 | case res.CodeSuccess: 29 | // 查询成功 30 | res.ResSuccess(c, data) 31 | } 32 | } 33 | 34 | // GetCDKData 获取CDK数据 35 | func GetCDKData(c *gin.Context) { 36 | method := c.Query("method") 37 | var data []model.CDK 38 | var resCode res.ResCode 39 | if method == "all" { 40 | // 查询全部数据 41 | resCode, data = logic.GetCDKData(1) 42 | } else if method == "true" { 43 | // 查询启用数据 44 | resCode, data = logic.GetCDKData(2) 45 | } else if method == "false" { 46 | // 查询禁用数据 47 | resCode, data = logic.GetCDKData(3) 48 | } else { 49 | // 查询搜索数据 50 | s := c.Query("s") 51 | zap.L().Debug("【CDK搜索】值:" + s) 52 | if s == "" { 53 | res.ResErrorWithMsg(c, res.CodeInvalidParam, "请求数据不完整") 54 | return 55 | } 56 | resCode, data = logic.GetOneCDKData(s) 57 | } 58 | 59 | switch resCode { 60 | case res.CodeSuccess: 61 | // 查询成功 62 | res.ResSuccess(c, data) 63 | } 64 | } 65 | 66 | // CreateCDKData 批量生成CDK 67 | func CreateCDKData(c *gin.Context) { 68 | // 获取参数 69 | p := new(model.CreateCDK) 70 | if err := c.ShouldBindJSON(&p); err != nil { 71 | // 参数校验 72 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 73 | 74 | // 判断err是不是validator.ValidationErrors类型 75 | errs, ok := err.(validator.ValidationErrors) 76 | if !ok { 77 | res.ResError(c, res.CodeInvalidParam) 78 | return 79 | } 80 | 81 | // 翻译错误 82 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 83 | return 84 | } 85 | 86 | // 处理业务 87 | resCode := logic.CreateCDKData(p) 88 | switch resCode { 89 | case res.CodeServerBusy: 90 | res.ResErrorWithMsg(c, res.CodeServerBusy, "生成CDK失败,请检查日志获取报错信息") 91 | case res.CodeCreateFileError: 92 | res.ResErrorWithMsg(c, res.CodeCreateFileError, "创建CDK已写入数据库,但生成下载文件失败") 93 | case res.CodeSuccess: 94 | // 生成成功 95 | res.ResSuccess(c, "生成成功") 96 | } 97 | } 98 | 99 | // DownloadCDKData 下载CDK文件 100 | func DownloadCDKData(c *gin.Context) { 101 | Filename := "CDK.txt" 102 | c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", Filename)) 103 | c.File("./" + Filename) 104 | go logic.DelCDKData() 105 | } 106 | 107 | // CDKState CDK全部启用/禁用 108 | func CDKState(c *gin.Context) { 109 | // 获取参数 110 | p := new(model.UpdateStateCDK) 111 | if err := c.ShouldBindJSON(&p); err != nil { 112 | // 参数校验 113 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 114 | 115 | // 判断err是不是validator.ValidationErrors类型 116 | errs, ok := err.(validator.ValidationErrors) 117 | if !ok { 118 | res.ResError(c, res.CodeInvalidParam) 119 | return 120 | } 121 | 122 | // 翻译错误 123 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 124 | return 125 | } 126 | 127 | // 处理业务 128 | resCode := logic.CDKState(p) 129 | switch resCode { 130 | case res.CodeSuccess: 131 | // 更新成功 132 | res.ResSuccess(c, "更新成功") 133 | } 134 | } 135 | 136 | // UpdateCDK 更新单条CDK数据 137 | func UpdateCDK(c *gin.Context) { 138 | // 获取参数 139 | p := new(model.UpdateCDK) 140 | if err := c.ShouldBindJSON(&p); err != nil { 141 | // 参数校验 142 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 143 | 144 | // 判断err是不是validator.ValidationErrors类型 145 | errs, ok := err.(validator.ValidationErrors) 146 | if !ok { 147 | res.ResError(c, res.CodeInvalidParam) 148 | return 149 | } 150 | 151 | // 翻译错误 152 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 153 | return 154 | } 155 | 156 | // 处理业务 157 | resCode := logic.UpdateCDK(p) 158 | switch resCode { 159 | case res.CodeSuccess: 160 | // 更新成功 161 | res.ResSuccess(c, "更新成功") 162 | } 163 | } 164 | 165 | // DelCDK 删除CDK数据 166 | func DelCDK(c *gin.Context) { 167 | // 获取参数 168 | p := new(model.DelCDK) 169 | if err := c.ShouldBindJSON(&p); err != nil { 170 | // 参数校验 171 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 172 | 173 | // 判断err是不是validator.ValidationErrors类型 174 | errs, ok := err.(validator.ValidationErrors) 175 | if !ok { 176 | res.ResError(c, res.CodeInvalidParam) 177 | return 178 | } 179 | 180 | // 翻译错误 181 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 182 | return 183 | } 184 | 185 | // 处理业务 186 | resCode := logic.DelCDK(p) 187 | switch resCode { 188 | case res.CodeSuccess: 189 | // 删除成功 190 | res.ResSuccess(c, "删除成功") 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /server/controllers/container.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/24 19:27 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : container.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | "QLPanelTools/server/sqlite" 13 | res "QLPanelTools/tools/response" 14 | val "QLPanelTools/tools/validator" 15 | "fmt" 16 | 17 | "github.com/gin-gonic/gin" 18 | "github.com/go-playground/validator/v10" 19 | "go.uber.org/zap" 20 | ) 21 | 22 | // Transfer 容器:迁移 23 | func Transfer(c *gin.Context) { 24 | // 获取参数 25 | p := new(model.TransferM) 26 | if err := c.ShouldBindJSON(&p); err != nil { 27 | // 参数校验 28 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 29 | 30 | // 判断err是不是validator.ValidationErrors类型 31 | errs, ok := err.(validator.ValidationErrors) 32 | if !ok { 33 | res.ResError(c, res.CodeInvalidParam) 34 | return 35 | } 36 | 37 | // 翻译错误 38 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 39 | return 40 | } 41 | 42 | // 处理业务 43 | resCode := logic.Transfer(p) 44 | switch resCode { 45 | case res.CodePanelNotWhitelisted: 46 | res.ResError(c, res.CodePanelNotWhitelisted) 47 | case res.CodeServerBusy: 48 | res.ResError(c, res.CodeServerBusy) 49 | case res.CodeSuccess: 50 | res.ResSuccess(c, "操作已进入任务队列, 请稍后前往青龙面板查看结果") 51 | } 52 | } 53 | 54 | // Copy 容器:复制 55 | func Copy(c *gin.Context) { 56 | // 获取参数 57 | p := new(model.CopyM) 58 | if err := c.ShouldBindJSON(&p); err != nil { 59 | // 参数校验 60 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 61 | 62 | // 判断err是不是validator.ValidationErrors类型 63 | errs, ok := err.(validator.ValidationErrors) 64 | if !ok { 65 | res.ResError(c, res.CodeInvalidParam) 66 | return 67 | } 68 | 69 | // 翻译错误 70 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 71 | return 72 | } 73 | 74 | // 处理业务 75 | resCode := logic.Copy(p) 76 | switch resCode { 77 | case res.CodePanelNotWhitelisted: 78 | res.ResError(c, res.CodePanelNotWhitelisted) 79 | case res.CodeServerBusy: 80 | res.ResError(c, res.CodeServerBusy) 81 | case res.CodeSuccess: 82 | res.ResSuccess(c, "操作已进入任务队列, 请稍后前往青龙面板查看结果") 83 | } 84 | } 85 | 86 | // Backup 容器:备份 87 | func Backup(c *gin.Context) { 88 | // 获取参数 89 | p := new(model.BackupM) 90 | if err := c.ShouldBindJSON(&p); err != nil { 91 | // 参数校验 92 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 93 | 94 | // 判断err是不是validator.ValidationErrors类型 95 | errs, ok := err.(validator.ValidationErrors) 96 | if !ok { 97 | res.ResError(c, res.CodeInvalidParam) 98 | return 99 | } 100 | 101 | // 翻译错误 102 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 103 | return 104 | } 105 | 106 | // 处理业务 107 | resCode := logic.Backup(p) 108 | switch resCode { 109 | case res.CodePanelNotWhitelisted: 110 | res.ResError(c, res.CodePanelNotWhitelisted) 111 | case res.CodeServerBusy: 112 | res.ResError(c, res.CodeServerBusy) 113 | case res.CodeSuccess: 114 | res.ResSuccess(c, "操作已进入任务队列, 完成后将弹出下载地址") 115 | } 116 | } 117 | 118 | // Restore 容器:恢复 119 | func Restore(c *gin.Context) { 120 | // 获取参数 121 | sID := c.Query("sID") 122 | file, _ := c.FormFile("file") 123 | zap.L().Debug(file.Filename) 124 | // 规范文件名称 125 | if file.Filename != "backup.json" { 126 | res.ResError(c, res.CodeNotStandardized) 127 | } 128 | 129 | // 保存文件 130 | err := c.SaveUploadedFile(file, "./"+file.Filename) 131 | if err != nil { 132 | // 记录错误 133 | sqlite.RecordingError("恢复任务", err.Error()) 134 | res.ResError(c, res.CodeServerBusy) 135 | } 136 | 137 | // 处理业务 138 | resCode := logic.Restore(sID) 139 | switch resCode { 140 | case res.CodePanelNotWhitelisted: 141 | res.ResError(c, res.CodePanelNotWhitelisted) 142 | case res.CodeServerBusy: 143 | res.ResError(c, res.CodeServerBusy) 144 | case res.CodeSuccess: 145 | res.ResSuccess(c, "操作已进入任务队列, 请稍后前往青龙面板查看结果") 146 | } 147 | } 148 | 149 | // BackupDownload 容器:备份数据下载 150 | func BackupDownload(c *gin.Context) { 151 | Filename := "backup.json" 152 | c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", Filename)) 153 | c.File("./" + Filename) 154 | go logic.DelBackupJSON() 155 | } 156 | 157 | // Info 容器:十条日志 158 | func Info(c *gin.Context) { 159 | // 处理业务 160 | info, resCode := logic.GetConInfo() 161 | switch resCode { 162 | case res.CodeSuccess: 163 | // 获取成功 164 | res.ResSuccess(c, info) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /server/controllers/env.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/6 16:48 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : env.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | res "QLPanelTools/tools/response" 13 | val "QLPanelTools/tools/validator" 14 | "github.com/gin-gonic/gin" 15 | "github.com/go-playground/validator/v10" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // EnvNameAdd 新增变量名 20 | func EnvNameAdd(c *gin.Context) { 21 | // 获取参数 22 | p := new(model.EnvNameAdd) 23 | if err := c.ShouldBindJSON(&p); err != nil { 24 | // 参数校验 25 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 26 | 27 | // 判断err是不是validator.ValidationErrors类型 28 | errs, ok := err.(validator.ValidationErrors) 29 | if !ok { 30 | res.ResError(c, res.CodeInvalidParam) 31 | return 32 | } 33 | 34 | // 翻译错误 35 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 36 | return 37 | } 38 | 39 | // 处理业务 40 | resCode := logic.EnvNameAdd(p) 41 | switch resCode { 42 | case res.CodeStorageFailed: 43 | // 变量名创建失败 44 | res.ResError(c, res.CodeStorageFailed) 45 | case res.CodeEnvNameExist: 46 | // 变量名已存在 47 | res.ResError(c, res.CodeEnvNameExist) 48 | case res.CodeSuccess: 49 | res.ResSuccess(c, "变量名创建成功") 50 | } 51 | } 52 | 53 | // EnvNameUp 修改变量名 54 | func EnvNameUp(c *gin.Context) { 55 | // 获取参数 56 | p := new(model.EnvNameUp) 57 | if err := c.ShouldBindJSON(&p); err != nil { 58 | // 参数校验 59 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 60 | 61 | // 判断err是不是validator.ValidationErrors类型 62 | errs, ok := err.(validator.ValidationErrors) 63 | if !ok { 64 | res.ResError(c, res.CodeInvalidParam) 65 | return 66 | } 67 | 68 | // 翻译错误 69 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 70 | return 71 | } 72 | 73 | // 处理业务 74 | resCode := logic.EnvNameUpdate(p) 75 | switch resCode { 76 | case res.CodeSuccess: 77 | res.ResSuccess(c, "变量名更新成功") 78 | } 79 | } 80 | 81 | // EnvNameDel 删除变量名 82 | func EnvNameDel(c *gin.Context) { 83 | // 获取参数 84 | p := new(model.EnvNameDel) 85 | if err := c.ShouldBindJSON(&p); err != nil { 86 | // 参数校验 87 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 88 | 89 | // 判断err是不是validator.ValidationErrors类型 90 | errs, ok := err.(validator.ValidationErrors) 91 | if !ok { 92 | res.ResError(c, res.CodeInvalidParam) 93 | return 94 | } 95 | 96 | // 翻译错误 97 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 98 | return 99 | } 100 | 101 | // 处理业务 102 | resCode := logic.EnvNameDel(p) 103 | switch resCode { 104 | case res.CodeSuccess: 105 | res.ResSuccess(c, "变量删除新成功") 106 | } 107 | } 108 | 109 | // GetAllEnvData 获取变量全部信息 110 | func GetAllEnvData(c *gin.Context) { 111 | // 处理业务 112 | env, resCode := logic.GetAllEnvData() 113 | switch resCode { 114 | case res.CodeSuccess: 115 | // 修改成功 116 | res.ResSuccess(c, env) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /server/controllers/javascript.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/6 16:04 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : javascript.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | res "QLPanelTools/tools/response" 12 | val "QLPanelTools/tools/validator" 13 | "io" 14 | "os" 15 | "path/filepath" 16 | "regexp" 17 | "strings" 18 | 19 | "github.com/gin-gonic/gin" 20 | "github.com/go-playground/validator/v10" 21 | "go.uber.org/zap" 22 | ) 23 | 24 | // JavascriptUpload 上传插件 25 | func JavascriptUpload(c *gin.Context) { 26 | // 获取上传文件 27 | file, err := c.FormFile("file") 28 | if err != nil { 29 | res.ResError(c, res.CodeServerBusy) 30 | return 31 | } 32 | 33 | // 获取插件目录绝对路径 34 | ExecPath, err := filepath.Abs(filepath.Dir(os.Args[0])) 35 | if err != nil { 36 | zap.L().Error(err.Error()) 37 | res.ResError(c, res.CodeServerBusy) 38 | return 39 | } 40 | 41 | // 保存文件 42 | FilePath := ExecPath + "/plugin/" + file.Filename 43 | err = c.SaveUploadedFile(file, FilePath) 44 | if err != nil { 45 | zap.L().Error(err.Error()) 46 | res.ResError(c, res.CodeServerBusy) 47 | return 48 | } 49 | 50 | res.ResSuccess(c, res.CodeSuccess) 51 | } 52 | 53 | // JavascriptDelete 删除插件 54 | func JavascriptDelete(c *gin.Context) { 55 | // 获取参数 56 | p := new(model.DeletePlugin) 57 | if err := c.ShouldBindJSON(&p); err != nil { 58 | // 参数校验 59 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 60 | 61 | // 判断err是不是validator.ValidationErrors类型 62 | errs, ok := err.(validator.ValidationErrors) 63 | if !ok { 64 | res.ResError(c, res.CodeInvalidParam) 65 | return 66 | } 67 | 68 | // 翻译错误 69 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 70 | return 71 | } 72 | 73 | // 获取插件目录绝对路径 74 | ExecPath, err := filepath.Abs(filepath.Dir(os.Args[0])) 75 | if err != nil { 76 | zap.L().Error(err.Error()) 77 | res.ResError(c, res.CodeServerBusy) 78 | return 79 | } 80 | 81 | // 删除文件 82 | FilePath := ExecPath + "/plugin/" + p.FileName 83 | err = os.Remove(FilePath) 84 | if err != nil { 85 | // 删除失败 86 | zap.L().Error(err.Error()) 87 | res.ResError(c, res.CodeRemoveFail) 88 | return 89 | } 90 | 91 | res.ResSuccess(c, res.CodeSuccess) 92 | } 93 | 94 | // JavascriptReadall 获取插件目录下所有插件 95 | func JavascriptReadall(c *gin.Context) { 96 | // 获取插件目录绝对路径 97 | ExecPath, err := filepath.Abs(filepath.Dir(os.Args[0])) 98 | if err != nil { 99 | zap.L().Error(err.Error()) 100 | res.ResError(c, res.CodeServerBusy) 101 | return 102 | } 103 | 104 | // 完整文件 105 | PluginPath := ExecPath + "/plugin/" 106 | 107 | // 读取目录 108 | var fl []model.FileInfo 109 | var fi model.FileInfo 110 | files, _ := os.ReadDir(PluginPath) 111 | for _, f := range files { 112 | // 跳过不是JS的文件 113 | if !strings.Contains(f.Name(), ".js") { 114 | continue 115 | } 116 | 117 | zap.L().Debug(f.Name()) 118 | 119 | // 读取插件名称 120 | fd, err := os.Open(PluginPath + f.Name()) 121 | if err != nil { 122 | zap.L().Error(f.Name() + ":打开文件失败") 123 | } 124 | defer fd.Close() 125 | v, _ := io.ReadAll(fd) 126 | data := string(v) 127 | FileIDName := "" 128 | if regs := regexp.MustCompile(`\[name:(.+)]`).FindStringSubmatch(data); len(regs) != 0 { 129 | FileIDName = strings.Trim(regs[1], " ") 130 | } 131 | 132 | fi.FileName = f.Name() 133 | fi.FileIDName = FileIDName 134 | fl = append(fl, fi) 135 | } 136 | 137 | res.ResSuccess(c, fl) 138 | } 139 | -------------------------------------------------------------------------------- /server/controllers/message.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/17 12:25 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : message.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | res "QLPanelTools/tools/response" 13 | val "QLPanelTools/tools/validator" 14 | "github.com/gin-gonic/gin" 15 | "github.com/go-playground/validator/v10" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // GetEmailData 获取邮件信息 20 | func GetEmailData(c *gin.Context) { 21 | resCode, data := logic.GetEmailData() 22 | switch resCode { 23 | case res.CodeSuccess: 24 | res.ResSuccess(c, data) 25 | } 26 | } 27 | 28 | // SendTestEmail 测试发送邮件 29 | func SendTestEmail(c *gin.Context) { 30 | // 获取参数 31 | p := new(model.TestEmail) 32 | 33 | if err := c.ShouldBindJSON(&p); err != nil { 34 | // 参数校验 35 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 36 | 37 | // 判断err是不是validator.ValidationErrors类型 38 | errs, ok := err.(validator.ValidationErrors) 39 | if !ok { 40 | res.ResError(c, res.CodeInvalidParam) 41 | return 42 | } 43 | 44 | // 翻译错误 45 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 46 | return 47 | } 48 | 49 | resCode := logic.TestEmailSend(p) 50 | switch resCode { 51 | case res.CodeServerBusy: 52 | res.ResErrorWithMsg(c, res.CodeServerBusy, "测试发送邮件失败,具体原因请查看日志记录") 53 | case res.CodeSuccess: 54 | res.ResSuccess(c, "测试邮件发送成功") 55 | } 56 | } 57 | 58 | // UpdateEmailSet 修改邮件配置 59 | func UpdateEmailSet(c *gin.Context) { 60 | // 获取参数 61 | p := new(model.UpdateEmail) 62 | if err := c.ShouldBindJSON(&p); err != nil { 63 | // 参数校验 64 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 65 | 66 | // 判断err是不是validator.ValidationErrors类型 67 | errs, ok := err.(validator.ValidationErrors) 68 | if !ok { 69 | res.ResError(c, res.CodeInvalidParam) 70 | return 71 | } 72 | 73 | // 翻译错误 74 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 75 | return 76 | } 77 | 78 | resCode := logic.UpdateEmailSet(p) 79 | switch resCode { 80 | case res.CodeSuccess: 81 | res.ResSuccess(c, "邮件服务信息修改成功") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /server/controllers/open.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:17 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : open.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | "QLPanelTools/server/sqlite" 13 | res "QLPanelTools/tools/response" 14 | val "QLPanelTools/tools/validator" 15 | "github.com/gin-gonic/gin" 16 | "github.com/go-playground/validator/v10" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | // SignUpHandle 注册请求 21 | func SignUpHandle(c *gin.Context) { 22 | // 获取参数 23 | p := new(model.UserSignUp) 24 | if err := c.ShouldBindJSON(&p); err != nil { 25 | // 参数校验 26 | zap.L().Error("SignUpHandle with invalid param", zap.Error(err)) 27 | 28 | // 判断err是不是validator.ValidationErrors类型 29 | errs, ok := err.(validator.ValidationErrors) 30 | if !ok { 31 | res.ResError(c, res.CodeInvalidParam) 32 | return 33 | } 34 | 35 | // 翻译错误 36 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 37 | return 38 | } 39 | 40 | // 业务处理 41 | resCode := logic.SignUp(p) 42 | switch resCode { 43 | case res.CodeServerBusy: 44 | // 内部服务错误 45 | res.ResError(c, res.CodeServerBusy) 46 | case res.CodeSuccess: 47 | // 注册成功 48 | res.ResSuccess(c, "注册完成") 49 | case res.CodeRegistrationClosed: 50 | // 已关闭注册,禁用注册 51 | res.ResError(c, res.CodeRegistrationClosed) 52 | } 53 | } 54 | 55 | // SignInHandle 登录请求 56 | func SignInHandle(c *gin.Context) { 57 | // 获取参数 58 | p := new(model.UserSignIn) 59 | if err := c.ShouldBindJSON(&p); err != nil { 60 | // 参数校验 61 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 62 | 63 | // 判断err是不是validator.ValidationErrors类型 64 | errs, ok := err.(validator.ValidationErrors) 65 | if !ok { 66 | res.ResError(c, res.CodeInvalidParam) 67 | return 68 | } 69 | 70 | // 翻译错误 71 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 72 | return 73 | } 74 | 75 | // 处理业务 76 | token, resCode := logic.SignIn(p) 77 | switch resCode { 78 | case res.CodeEmailNotExist: 79 | go logic.AddIPAddr(c.ClientIP(), false) 80 | // 邮箱不存在 81 | res.ResError(c, res.CodeEmailNotExist) 82 | case res.CodeEmailFormatError: 83 | go logic.AddIPAddr(c.ClientIP(), false) 84 | // 邮箱格式错误 85 | res.ResError(c, res.CodeEmailFormatError) 86 | case res.CodeInvalidPassword: 87 | go logic.AddIPAddr(c.ClientIP(), false) 88 | // 邮箱或者密码错误 89 | res.ResError(c, res.CodeInvalidPassword) 90 | case res.CodeServerBusy: 91 | go logic.AddIPAddr(c.ClientIP(), false) 92 | // 生成Token出错 93 | res.ResError(c, res.CodeServerBusy) 94 | case res.CodeSuccess: 95 | go logic.AddIPAddr(c.ClientIP(), true) 96 | // 登录成功,返回Token 97 | res.ResSuccess(c, token) 98 | } 99 | } 100 | 101 | // CheckToken 检查Token是否有效 102 | func CheckToken(c *gin.Context) { 103 | // 获取参数 104 | p := new(model.CheckToken) 105 | if err := c.ShouldBindJSON(&p); err != nil { 106 | // 参数校验 107 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 108 | 109 | // 判断err是不是validator.ValidationErrors类型 110 | errs, ok := err.(validator.ValidationErrors) 111 | if !ok { 112 | res.ResError(c, res.CodeInvalidParam) 113 | return 114 | } 115 | 116 | // 翻译错误 117 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 118 | return 119 | } 120 | 121 | // 处理业务 122 | bol, resCode := logic.CheckToken(p) 123 | switch resCode { 124 | case res.CodeServerBusy: 125 | // 内部服务错误 126 | res.ResError(c, res.CodeServerBusy) 127 | case res.CodeInvalidToken: 128 | // Token已失效 129 | res.ResErrorWithMsg(c, res.CodeInvalidToken, "无效的Token") 130 | case res.CodeSuccess: 131 | // 上传成功 132 | res.ResSuccess(c, bol) 133 | } 134 | } 135 | 136 | // IndexData 可用服务 137 | func IndexData(c *gin.Context) { 138 | // 处理业务 139 | resCode, data := logic.EnvData() 140 | switch resCode { 141 | case res.CodeDataError: 142 | res.ResErrorWithMsg(c, res.CodeDataError, "发生一点小意外,请刷新页面") 143 | case res.CodeCheckDataNotExist: 144 | // 获取数据不存在 145 | res.ResError(c, res.CodeCheckDataNotExist) 146 | case res.CodeServerBusy: 147 | // 内部服务错误 148 | res.ResError(c, res.CodeServerBusy) 149 | case res.CodeSuccess: 150 | // 获取成功,返回内容 151 | res.ResSuccess(c, data) 152 | } 153 | } 154 | 155 | // EnvADD 上传变量 156 | func EnvADD(c *gin.Context) { 157 | // 获取参数 158 | p := new(model.EnvAdd) 159 | if err := c.ShouldBindJSON(&p); err != nil { 160 | // 参数校验 161 | zap.L().Error("SignUpHandle with invalid param", zap.Error(err)) 162 | 163 | // 判断err是不是validator.ValidationErrors类型 164 | errs, ok := err.(validator.ValidationErrors) 165 | if !ok { 166 | res.ResError(c, res.CodeInvalidParam) 167 | return 168 | } 169 | 170 | // 翻译错误 171 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 172 | return 173 | } 174 | // 查询请求IP是否受限 175 | resCode := logic.CheckIPIfItNormal(c.ClientIP()) 176 | if resCode == res.CodeNumberDepletion { 177 | res.ResErrorWithMsg(c, res.CodeNumberDepletion, "今日提交已到达上限") 178 | return 179 | } else if resCode == res.CodeServerBusy { 180 | res.ResErrorWithMsg(c, res.CodeServerBusy, "服务繁忙,请稍后重试") 181 | return 182 | } 183 | // 业务处理 184 | resCode, msg := logic.EnvAdd(p) 185 | switch resCode { 186 | case res.CodeServerBusy: 187 | res.ResErrorWithMsg(c, res.CodeServerBusy, "服务繁忙,请稍后重试") 188 | case res.CodeStorageFailed: 189 | res.ResErrorWithMsg(c, res.CodeStorageFailed, msg) 190 | case res.CodeErrorOccurredInTheRequest: 191 | res.ResErrorWithMsg(c, res.CodeErrorOccurredInTheRequest, "提交服务器或变量名不在白名单") 192 | case res.CodeEnvDataMismatch: 193 | res.ResErrorWithMsg(c, res.CodeEnvDataMismatch, "上传内容不符合规定, 请检查后再提交") 194 | case res.CodeDataIsNull: 195 | res.ResErrorWithMsg(c, res.CodeDataIsNull, "上传内容不能为空") 196 | case res.CodeLocationFull: 197 | res.ResErrorWithMsg(c, res.CodeLocationFull, "限额已满,禁止提交") 198 | case res.CodeNoDuplicateSubmission: 199 | res.ResErrorWithMsg(c, res.CodeNoDuplicateSubmission, "禁止提交重复数据") 200 | case res.CodeBlackListEnv: 201 | res.ResErrorWithMsg(c, res.CodeBlackListEnv, "变量已被管理员禁止提交") 202 | case res.CodeCustomError: 203 | // JS执行发生错误, 系统错误 204 | res.ResErrorWithMsg(c, res.CodeCustomError, "执行插件发生错误,错误原因:"+msg) 205 | case res.CodeNoAdmittance: 206 | // 数据禁止通过 207 | res.ResErrorWithMsg(c, res.CodeNoAdmittance, msg) 208 | case res.CodeCDKError: 209 | res.ResErrorWithMsg(c, res.CodeCDKError, msg) 210 | case res.CodeSuccess: 211 | // 记录上传IP 212 | go sqlite.InsertSubmitRecord(c.ClientIP()) 213 | // 更新CDK使用次数 214 | go sqlite.UpdateCDKAvailableTimes(p) 215 | // 上传成功 216 | res.ResSuccess(c, "上传成功") 217 | } 218 | } 219 | 220 | // CheckCDK CDK检查 221 | func CheckCDK(c *gin.Context) { 222 | // 获取参数 223 | p := new(model.CheckCDK) 224 | if err := c.ShouldBindJSON(&p); err != nil { 225 | // 参数校验 226 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 227 | 228 | // 判断err是不是validator.ValidationErrors类型 229 | errs, ok := err.(validator.ValidationErrors) 230 | if !ok { 231 | res.ResError(c, res.CodeInvalidParam) 232 | return 233 | } 234 | 235 | // 翻译错误 236 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 237 | return 238 | } 239 | 240 | // 处理业务 241 | resCode, str := logic.CheckCDK(p) 242 | switch resCode { 243 | case res.CodeSuccess: 244 | // 上传成功 245 | res.ResSuccess(c, str) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /server/controllers/panel.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/7 16:39 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : panel.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | res "QLPanelTools/tools/response" 13 | val "QLPanelTools/tools/validator" 14 | "fmt" 15 | "github.com/gin-gonic/gin" 16 | "github.com/go-playground/validator/v10" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | // PanelAdd 新增变量名 21 | func PanelAdd(c *gin.Context) { 22 | // 获取参数 23 | p := new(model.PanelData) 24 | if err := c.ShouldBindJSON(&p); err != nil { 25 | // 参数校验 26 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 27 | 28 | // 判断err是不是validator.ValidationErrors类型 29 | errs, ok := err.(validator.ValidationErrors) 30 | if !ok { 31 | res.ResError(c, res.CodeInvalidParam) 32 | return 33 | } 34 | 35 | // 翻译错误 36 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 37 | return 38 | } 39 | 40 | // 处理业务 41 | resCode := logic.PanelAdd(p) 42 | switch resCode { 43 | case res.CodeStorageFailed: 44 | // 创建面板信息出错 45 | res.ResError(c, res.CodeStorageFailed) 46 | case res.CodeSuccess: 47 | res.ResSuccess(c, "面板信息创建成功") 48 | } 49 | } 50 | 51 | // PanelUp 修改变量名 52 | func PanelUp(c *gin.Context) { 53 | // 获取参数 54 | p := new(model.UpPanelData) 55 | if err := c.ShouldBindJSON(&p); err != nil { 56 | fmt.Println(p) 57 | // 参数校验 58 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 59 | 60 | // 判断err是不是validator.ValidationErrors类型 61 | errs, ok := err.(validator.ValidationErrors) 62 | if !ok { 63 | res.ResError(c, res.CodeInvalidParam) 64 | return 65 | } 66 | 67 | // 翻译错误 68 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 69 | return 70 | } 71 | 72 | // 处理业务 73 | resCode := logic.PanelUpdate(p) 74 | switch resCode { 75 | case res.CodeSuccess: 76 | res.ResSuccess(c, "面板信息更新成功") 77 | } 78 | } 79 | 80 | // PanelDel 删除变量名 81 | func PanelDel(c *gin.Context) { 82 | // 获取参数 83 | p := new(model.DelPanelData) 84 | if err := c.ShouldBindJSON(&p); err != nil { 85 | // 参数校验 86 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 87 | 88 | // 判断err是不是validator.ValidationErrors类型 89 | errs, ok := err.(validator.ValidationErrors) 90 | if !ok { 91 | res.ResError(c, res.CodeInvalidParam) 92 | return 93 | } 94 | 95 | // 翻译错误 96 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 97 | return 98 | } 99 | 100 | // 处理业务 101 | resCode := logic.PanelDelete(p) 102 | switch resCode { 103 | case res.CodeSuccess: 104 | res.ResSuccess(c, "面板信息删除成功") 105 | } 106 | } 107 | 108 | // GetAllPanelData 获取面板全部信息 109 | func GetAllPanelData(c *gin.Context) { 110 | // 处理业务 111 | env, resCode := logic.GetAllPanelData() 112 | switch resCode { 113 | case res.CodeSuccess: 114 | // 修改成功 115 | res.ResSuccess(c, env) 116 | } 117 | } 118 | 119 | // UpdatePanelEnvData 修改面板绑定变量 120 | func UpdatePanelEnvData(c *gin.Context) { 121 | // 获取参数 122 | p := new(model.PanelEnvData) 123 | if err := c.ShouldBindJSON(&p); err != nil { 124 | // 参数校验 125 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 126 | 127 | // 判断err是不是validator.ValidationErrors类型 128 | errs, ok := err.(validator.ValidationErrors) 129 | if !ok { 130 | res.ResError(c, res.CodeInvalidParam) 131 | return 132 | } 133 | 134 | // 翻译错误 135 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 136 | return 137 | } 138 | 139 | // 处理业务 140 | resCode := logic.UpdatePanelEnvData(p) 141 | switch resCode { 142 | case res.CodeSuccess: 143 | // 修改成功 144 | res.ResSuccess(c, "修改成功") 145 | } 146 | } 147 | 148 | // UnbindPanelEnvData 解除所有面板变量绑定 149 | func UnbindPanelEnvData(c *gin.Context) { 150 | // 处理业务 151 | resCode := logic.UnbindPanelEnvData() 152 | switch resCode { 153 | case res.CodeSuccess: 154 | // 解绑成功 155 | res.ResSuccess(c, "解绑成功") 156 | } 157 | } 158 | 159 | // UpdateAllPanelToken 批量更新面板Token 160 | func UpdateAllPanelToken(c *gin.Context) { 161 | // 处理业务 162 | resCode := logic.UpdateAllPanelToken() 163 | switch resCode { 164 | case res.CodeSuccess: 165 | // 更新成功 166 | res.ResSuccess(c, "批量更新面板Token成功") 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /server/controllers/setting.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/21 19:47 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : setting.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | res "QLPanelTools/tools/response" 13 | val "QLPanelTools/tools/validator" 14 | "github.com/gin-gonic/gin" 15 | "github.com/go-playground/validator/v10" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // GetSetting 获取单个配置 20 | func GetSetting(c *gin.Context) { 21 | key := c.Query("key") 22 | data, resCode := logic.GetSetting(key) 23 | switch resCode { 24 | case res.CodeServerBusy: 25 | // 越权 26 | res.ResErrorWithMsg(c, res.CodeServerBusy, "获取内容为空") 27 | case res.CodeSuccess: 28 | // 获取成功 29 | res.ResSuccess(c, data) 30 | } 31 | } 32 | 33 | // GetSettings 获取全部配置 34 | func GetSettings(c *gin.Context) { 35 | data, resCode := logic.GetSettings() 36 | switch resCode { 37 | case res.CodeServerBusy: 38 | res.ResError(c, res.CodeServerBusy) 39 | case res.CodeSuccess: 40 | // 获取成功 41 | res.ResSuccess(c, data) 42 | } 43 | } 44 | 45 | // SaveSettings 保存网站信息 46 | func SaveSettings(c *gin.Context) { 47 | p := new([]model.WebSettings) 48 | if err := c.ShouldBindJSON(&p); err != nil { 49 | // 参数校验 50 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 51 | 52 | // 判断err是不是validator.ValidationErrors类型 53 | errs, ok := err.(validator.ValidationErrors) 54 | if !ok { 55 | res.ResError(c, res.CodeInvalidParam) 56 | return 57 | } 58 | 59 | // 翻译错误 60 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 61 | return 62 | } 63 | 64 | // 处理业务 65 | resCode := logic.SaveSettings(p) 66 | switch resCode { 67 | case res.CodeServerBusy: 68 | res.ResError(c, res.CodeServerBusy) 69 | case res.CodeSuccess: 70 | // 修改成功 71 | res.ResSuccess(c, "保存成功") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /server/controllers/system.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/23 15:30 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : system.go 6 | 7 | package controllers 8 | 9 | import ( 10 | "QLPanelTools/server/logic" 11 | "QLPanelTools/server/model" 12 | res "QLPanelTools/tools/response" 13 | val "QLPanelTools/tools/validator" 14 | "github.com/gin-gonic/gin" 15 | "github.com/go-playground/validator/v10" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // CheckVersion 检查版本更新 20 | func CheckVersion(c *gin.Context) { 21 | v, resCode := logic.CheckVersion() 22 | switch resCode { 23 | case res.CodeServerBusy: 24 | res.ResError(c, res.CodeServerBusy) 25 | case res.CodeSuccess: 26 | // 获取成功 27 | res.ResSuccess(c, v) 28 | } 29 | } 30 | 31 | // UpdateSoftware 更新软件 32 | func UpdateSoftware(c *gin.Context) { 33 | // 获取参数 34 | p := new(model.SoftWareGOOS) 35 | if err := c.ShouldBindJSON(&p); err != nil { 36 | // 参数校验 37 | zap.L().Error("SignInHandle with invalid param", zap.Error(err)) 38 | 39 | // 判断err是不是validator.ValidationErrors类型 40 | errs, ok := err.(validator.ValidationErrors) 41 | if !ok { 42 | res.ResError(c, res.CodeInvalidParam) 43 | return 44 | } 45 | 46 | // 翻译错误 47 | res.ResErrorWithMsg(c, res.CodeInvalidParam, val.RemoveTopStruct(errs.Translate(val.Trans))) 48 | return 49 | } 50 | 51 | resCode, txt := logic.UpdateSoftware(p) 52 | switch resCode { 53 | case res.CodeUpdateServerBusy: 54 | res.ResErrorWithMsg(c, res.CodeUpdateServerBusy, txt) 55 | case res.CodeSuccess: 56 | // 获取成功 57 | res.ResSuccess(c, txt) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/logic/cdkLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/12 14:33 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : cdkLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | res "QLPanelTools/tools/response" 13 | "bufio" 14 | "os" 15 | "strconv" 16 | "time" 17 | 18 | "github.com/segmentio/ksuid" 19 | "go.uber.org/zap" 20 | ) 21 | 22 | // GetDivisionCDKData CDK:以20条数据分割 23 | func GetDivisionCDKData(page string) (res.ResCode, model.CDKPageData) { 24 | var data []model.CDK 25 | var cdkPage model.CDKPageData 26 | if page == "" { 27 | // 空值,默认获取前20条数据(第一页) 28 | data = sqlite.GetDivisionCDKData(1) 29 | } else { 30 | // String转Int 31 | intPage, err := strconv.Atoi(page) 32 | if err != nil { 33 | // 类型转换失败,查询默认获取前20条数据(第一页) 34 | zap.L().Error(err.Error()) 35 | data = sqlite.GetDivisionCDKData(1) 36 | } else { 37 | // 查询指定页数的数据 38 | data = sqlite.GetDivisionCDKData(intPage) 39 | } 40 | } 41 | 42 | // 查询总页数 43 | count := sqlite.GetCDKDataPage() 44 | // 计算页数 45 | z := count / 20 46 | var y int64 47 | y = count % 20 48 | 49 | if y != 0 { 50 | cdkPage.Page = z + 1 51 | } else { 52 | cdkPage.Page = z 53 | } 54 | cdkPage.CDKData = data 55 | 56 | return res.CodeSuccess, cdkPage 57 | } 58 | 59 | // GetCDKData 条件查询CDK数据 60 | func GetCDKData(state int) (res.ResCode, []model.CDK) { 61 | // state 1:全部、2:启用、3:禁用 62 | var data []model.CDK 63 | if state == 1 { 64 | data = sqlite.GetAllCDKData() 65 | } else if state == 2 { 66 | data = sqlite.GetTrueCDKData() 67 | } else { 68 | data = sqlite.GetFalseCDKData() 69 | } 70 | 71 | return res.CodeSuccess, data 72 | } 73 | 74 | // GetOneCDKData 搜索CDK数据 75 | func GetOneCDKData(cdk string) (res.ResCode, []model.CDK) { 76 | return res.CodeSuccess, sqlite.GetOneCDKData(cdk) 77 | } 78 | 79 | // CreateCDKData 生成CDK数据 80 | func CreateCDKData(p *model.CreateCDK) res.ResCode { 81 | // 判断本地是否还有遗留文件 82 | _, err := os.Stat("CDK.txt") 83 | if err == nil { 84 | // 删除旧文件 85 | err := os.Remove("CDK.txt") 86 | if err != nil { 87 | zap.L().Error(err.Error()) 88 | return res.CodeServerBusy 89 | } 90 | } 91 | 92 | // 创建记录数组 93 | var li []string 94 | 95 | // 创建对象 96 | cdk := new(model.CDK) 97 | 98 | cdk.AvailableTimes = p.CdKeyAvailableTimes 99 | cdk.State = true 100 | // 获取生成数量 101 | for i := 0; i < p.CdKeyCount; i++ { 102 | // 生成用户UID 103 | uid := ksuid.New() 104 | cdk.CdKey = uid.String() 105 | 106 | // 加入数组 107 | li = append(li, cdk.CdKey) 108 | 109 | // 写入数据库 110 | sqlite.InsertCDKData(cdk) 111 | } 112 | 113 | // 创建CDK.txt并写入数据 114 | filepath := "CDK.txt" 115 | file, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0) 116 | if err != nil { 117 | zap.L().Error(err.Error()) 118 | return res.CodeCreateFileError 119 | } 120 | defer file.Close() 121 | 122 | // 写入CDK数据 123 | writer := bufio.NewWriter(file) 124 | for i := 0; i < len(li); i++ { 125 | _, err2 := writer.WriteString(li[i] + "\n") 126 | if err2 != nil { 127 | zap.L().Error(err2.Error()) 128 | return res.CodeCreateFileError 129 | } 130 | } 131 | err = writer.Flush() 132 | if err != nil { 133 | zap.L().Error(err.Error()) 134 | return res.CodeCreateFileError 135 | } 136 | 137 | return res.CodeSuccess 138 | } 139 | 140 | // DelCDKData 删除本地数据 141 | func DelCDKData() { 142 | time.Sleep(time.Second * 10) 143 | err := os.Remove("CDK.txt") 144 | if err != nil { 145 | zap.L().Error(err.Error()) 146 | } 147 | } 148 | 149 | // CDKState CDK全部启用/禁用 150 | func CDKState(p *model.UpdateStateCDK) res.ResCode { 151 | sqlite.UpdateCDKDataState(p) 152 | return res.CodeSuccess 153 | } 154 | 155 | // UpdateCDK 更新单条CDK数据 156 | func UpdateCDK(p *model.UpdateCDK) res.ResCode { 157 | sqlite.UpdateCDKData(p) 158 | return res.CodeSuccess 159 | } 160 | 161 | // DelCDK 删除CDK 162 | func DelCDK(p *model.DelCDK) res.ResCode { 163 | sqlite.DelCDKData(p) 164 | return res.CodeSuccess 165 | } 166 | -------------------------------------------------------------------------------- /server/logic/containerLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/24 19:31 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : containerLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | "QLPanelTools/tools/panel" 13 | "QLPanelTools/tools/requests" 14 | res "QLPanelTools/tools/response" 15 | "encoding/json" 16 | "go.uber.org/zap" 17 | "io/ioutil" 18 | "os" 19 | "strconv" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | // Transfer 容器:迁移 25 | func Transfer(p *model.TransferM) res.ResCode { 26 | // 根据ID查询服务器信息 27 | oneData := sqlite.GetPanelDataByID(p.IDOne) 28 | twoData := sqlite.GetPanelDataByID(p.IDTwo) 29 | 30 | // 检查白名单 31 | if oneData.URL == "" { 32 | return res.CodePanelNotWhitelisted 33 | } 34 | if twoData.URL == "" { 35 | return res.CodePanelNotWhitelisted 36 | } 37 | 38 | // 为了保证服务高可用性,强制更新面板Token 39 | panel.GetPanelToken(oneData.URL, oneData.ClientID, oneData.ClientSecret) 40 | panel.GetPanelToken(twoData.URL, twoData.ClientID, twoData.ClientSecret) 41 | time.Sleep(time.Second) 42 | 43 | // 重新获取Token 44 | one := sqlite.GetPanelDataByID(p.IDOne) 45 | two := sqlite.GetPanelDataByID(p.IDTwo) 46 | 47 | // 获取One面板全部信息 48 | zap.L().Debug("容器迁移:获取One面板全部信息") 49 | url := panel.StringHTTP(one.URL) + "/open/envs?searchValue=&t=" + strconv.Itoa(one.Params) 50 | allData, _ := requests.Requests("GET", url, "", one.Token) 51 | 52 | // 绑定数据 53 | var token model.PanelAllEnv 54 | err := json.Unmarshal(allData, &token) 55 | if err != nil { 56 | zap.L().Error(err.Error()) 57 | return res.CodeServerBusy 58 | } 59 | 60 | // 向B容器上传变量 61 | zap.L().Debug("容器迁移:向B容器上传变量") 62 | go EnvUp(token, two.URL, two.Token, two.Params, "迁移任务(上传)") 63 | 64 | // 获取A容器所有变量ID 65 | idGroup := `[` 66 | for i := 0; i < len(token.Data); i++ { 67 | idGroup = idGroup + strconv.Itoa(token.Data[i].ID) + `,` 68 | } 69 | idGroup = idGroup[:len(idGroup)-1] 70 | idGroup = idGroup + `]` 71 | 72 | // 删除A容器变量 73 | zap.L().Debug("容器迁移:删除A容器变量") 74 | EnvDel(idGroup, one.URL, one.Token, one.Params, "迁移任务(删除)") 75 | 76 | return res.CodeSuccess 77 | } 78 | 79 | // Copy 容器:复制 80 | func Copy(p *model.CopyM) res.ResCode { 81 | // 根据ID查询服务器信息 82 | oneData := sqlite.GetPanelDataByID(p.IDOne) 83 | twoData := sqlite.GetPanelDataByID(p.IDTwo) 84 | 85 | // 检查白名单 86 | if oneData.URL == "" { 87 | return res.CodePanelNotWhitelisted 88 | } 89 | if twoData.URL == "" { 90 | return res.CodePanelNotWhitelisted 91 | } 92 | 93 | // 为了保证服务高可用性,强制更新面板Token 94 | panel.GetPanelToken(oneData.URL, oneData.ClientID, oneData.ClientSecret) 95 | panel.GetPanelToken(twoData.URL, twoData.ClientID, twoData.ClientSecret) 96 | time.Sleep(time.Second) 97 | 98 | // 重新获取Token 99 | one := sqlite.GetPanelDataByID(p.IDOne) 100 | two := sqlite.GetPanelDataByID(p.IDTwo) 101 | 102 | // 获取One面板全部信息 103 | zap.L().Debug("容器复制:获取One面板全部信息") 104 | url := panel.StringHTTP(one.URL) + "/open/envs?searchValue=&t=" + strconv.Itoa(one.Params) 105 | allData, _ := requests.Requests("GET", url, "", one.Token) 106 | 107 | // 绑定数据 108 | var token model.PanelAllEnv 109 | err := json.Unmarshal(allData, &token) 110 | if err != nil { 111 | zap.L().Error(err.Error()) 112 | return res.CodeServerBusy 113 | } 114 | 115 | // 向B容器上传变量 116 | zap.L().Debug("容器复制:向B容器上传变量") 117 | go EnvUp(token, two.URL, two.Token, two.Params, "复制任务(上传)") 118 | 119 | return res.CodeSuccess 120 | } 121 | 122 | // Backup 容器:备份 123 | func Backup(p *model.BackupM) res.ResCode { 124 | // 根据ID查询服务器信息 125 | one := sqlite.GetPanelDataByID(p.IDOne) 126 | 127 | // 检查白名单 128 | if one.URL == "" { 129 | return res.CodePanelNotWhitelisted 130 | } 131 | 132 | // 为了保证服务高可用性,强制更新面板Token 133 | panel.GetPanelToken(one.URL, one.ClientID, one.ClientSecret) 134 | time.Sleep(time.Second) 135 | 136 | // 重新获取Token 137 | server := sqlite.GetPanelDataByID(p.IDOne) 138 | 139 | // 获取One面板全部信息 140 | zap.L().Debug("容器备份:获取面板全部信息") 141 | url := panel.StringHTTP(server.URL) + "/open/envs?searchValue=&t=" + strconv.Itoa(server.Params) 142 | allData, _ := requests.Requests("GET", url, "", server.Token) 143 | 144 | // 绑定数据 145 | var token model.PanelAllEnv 146 | err := json.Unmarshal(allData, &token) 147 | if err != nil { 148 | // 记录错误 149 | sqlite.RecordingError("备份任务", err.Error()) 150 | zap.L().Error(err.Error()) 151 | return res.CodeServerBusy 152 | } 153 | 154 | // 创建JSON文件 155 | _, err = os.Create("backup.json") 156 | if err != nil { 157 | // 记录错误 158 | sqlite.RecordingError("备份任务", err.Error()) 159 | zap.L().Error(err.Error()) 160 | return res.CodeServerBusy 161 | } 162 | 163 | // 打开JSON文件 164 | f, err := os.Open("backup.json") 165 | if err != nil { 166 | // 记录错误 167 | sqlite.RecordingError("备份任务", err.Error()) 168 | zap.L().Error(err.Error()) 169 | return res.CodeServerBusy 170 | } 171 | defer func(f *os.File) { 172 | err = f.Close() 173 | if err != nil { 174 | // 记录错误 175 | sqlite.RecordingError("备份任务", err.Error()) 176 | zap.L().Error(err.Error()) 177 | } 178 | }(f) 179 | 180 | // 序列化数据 181 | b, err := json.Marshal(token.Data) 182 | if err != nil { 183 | // 记录错误 184 | sqlite.RecordingError("备份任务", err.Error()) 185 | zap.L().Error(err.Error()) 186 | return res.CodeServerBusy 187 | } 188 | 189 | // 保存数据 190 | err = ioutil.WriteFile("backup.json", b, 0777) 191 | if err != nil { 192 | // 记录错误 193 | sqlite.RecordingError("备份任务", err.Error()) 194 | zap.L().Error(err.Error()) 195 | return res.CodeServerBusy 196 | } 197 | 198 | return res.CodeSuccess 199 | } 200 | 201 | // Restore 容器:恢复 202 | func Restore(sID string) res.ResCode { 203 | // 根据ID查询服务器信息 204 | iID, err := strconv.Atoi(sID) 205 | if err != nil { 206 | zap.L().Error(err.Error()) 207 | return res.CodeServerBusy 208 | } 209 | one := sqlite.GetPanelDataByID(iID) 210 | 211 | // 检查白名单 212 | if one.URL == "" { 213 | return res.CodePanelNotWhitelisted 214 | } 215 | 216 | // 为了保证服务高可用性,强制更新面板Token 217 | panel.GetPanelToken(one.URL, one.ClientID, one.ClientSecret) 218 | time.Sleep(time.Second) 219 | 220 | // 读取本地数据 221 | var backup model.PanelAllEnv 222 | // 打开文件 223 | file, err := os.Open("./backup.json") 224 | if err != nil { 225 | // 打开文件时发生错误 226 | zap.L().Error(err.Error()) 227 | return res.CodeServerBusy 228 | } 229 | // 延迟关闭 230 | defer func(file *os.File) { 231 | err := file.Close() 232 | if err != nil { 233 | zap.L().Error(err.Error()) 234 | } 235 | }(file) 236 | 237 | // 配置读取 238 | byteData, err2 := ioutil.ReadAll(file) 239 | if err2 != nil { 240 | // 读取配置时发生错误 241 | zap.L().Error(err.Error()) 242 | return res.CodeServerBusy 243 | } 244 | 245 | // 数据绑定 246 | err3 := json.Unmarshal(byteData, &backup.Data) 247 | if err3 != nil { 248 | // 数据绑定时发生错误 249 | zap.L().Error(err.Error()) 250 | return res.CodeServerBusy 251 | } 252 | 253 | // 上传数据 254 | go EnvUp(backup, one.URL, one.Token, one.Params, "恢复任务") 255 | // 删除本地数据 256 | go DelBackupJSON() 257 | return res.CodeSuccess 258 | } 259 | 260 | // EnvUp 变量上传 261 | func EnvUp(p model.PanelAllEnv, url, token string, params int, journal string) { 262 | var re int 263 | var panelData model.QLPanel 264 | 265 | for i := 0; i < len(p.Data); i++ { 266 | var data string 267 | var pRes model.PanelRes 268 | 269 | if re == 1 { 270 | panelData = sqlite.GetPanelDataByURL(url) 271 | } 272 | 273 | zap.L().Debug("URL地址:" + url) 274 | URL := panel.StringHTTP(url) + "/open/envs?t=" + strconv.Itoa(params) 275 | // 将字符串里面的双引号添加转义 276 | p.Data[i].Value = strings.ReplaceAll(p.Data[i].Value, `"`, `\"`) 277 | // 上传 278 | data = `[{"value": "` + p.Data[i].Value + `","name": "` + p.Data[i].Name + `","remarks": "` + p.Data[i].Remarks + `"}]` 279 | // 执行上传任务 280 | r, err := requests.Requests("POST", URL, data, token) 281 | if err != nil { 282 | // 记录错误 283 | zap.L().Error("[容器:变量:上传]请求发送失败:" + err.Error()) 284 | sqlite.RecordingError(journal, err.Error()) 285 | } 286 | // 序列化内容 287 | err = json.Unmarshal(r, &pRes) 288 | if err != nil { 289 | zap.L().Error(err.Error()) 290 | } 291 | 292 | if pRes.Code >= 401 && pRes.Code <= 500 { 293 | // 更新Token, 再次提交 294 | _, t := panel.GetPanelToken(panel.StringHTTP(url), panelData.ClientID, panelData.ClientSecret) 295 | token = t.Data.Token 296 | params = t.Data.Expiration 297 | re = 1 298 | i -= 1 299 | } else if pRes.Code == 400 { 300 | // 可能是重复上传,跳过 301 | continue 302 | } else if pRes.Code >= 500 { 303 | // 青龙请求错误, 再次提交 304 | i -= 1 305 | } else if pRes.Code == 200 { 306 | re = 0 307 | } 308 | 309 | // 限速 310 | time.Sleep(time.Second / 8) 311 | } 312 | } 313 | 314 | // EnvDel 变量删除 315 | func EnvDel(p string, url, token string, params int, journal string) { 316 | URL := panel.StringHTTP(url) + "/open/envs?t=" + strconv.Itoa(params) 317 | // 执行删除任务 318 | _, err := requests.Requests("DELETE", URL, p, token) 319 | if err != nil { 320 | // 记录错误 321 | sqlite.RecordingError(journal, err.Error()) 322 | } 323 | } 324 | 325 | // GetConInfo 获取十条日志记录 326 | func GetConInfo() ([]model.OperationRecord, res.ResCode) { 327 | // 查询记录 328 | info := sqlite.GetConData() 329 | return info, res.CodeSuccess 330 | } 331 | 332 | // DelBackupJSON 删除本地数据 333 | func DelBackupJSON() { 334 | time.Sleep(time.Second * 10) 335 | err := os.Remove("backup.json") 336 | if err != nil { 337 | zap.L().Error(err.Error()) 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /server/logic/envLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/6 17:04 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : envLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | res "QLPanelTools/tools/response" 13 | ) 14 | 15 | // EnvNameAdd 创建变量名 16 | func EnvNameAdd(p *model.EnvNameAdd) res.ResCode { 17 | // 检查变量名是否已存在 18 | result := sqlite.CheckEnvName(p.EnvName) 19 | if result == true { 20 | return res.CodeEnvNameExist 21 | } 22 | 23 | // 不存在, 创建新的变量名 24 | err := sqlite.AddEnvName(p) 25 | if err != nil { 26 | return res.CodeStorageFailed 27 | } 28 | return res.CodeSuccess 29 | } 30 | 31 | // EnvNameUpdate 更新变量名 32 | func EnvNameUpdate(p *model.EnvNameUp) res.ResCode { 33 | // 更新数据库 34 | sqlite.UpdateEnvName(p) 35 | return res.CodeSuccess 36 | } 37 | 38 | // EnvNameDel 删除变量名 39 | func EnvNameDel(p *model.EnvNameDel) res.ResCode { 40 | // 删除变量名 41 | sqlite.DelEnvName(p) 42 | return res.CodeSuccess 43 | } 44 | 45 | // GetAllEnvData 获取变量全部信息 46 | func GetAllEnvData() ([]model.EnvName, res.ResCode) { 47 | // 获取信息 48 | env := sqlite.GetEnvNameAll() 49 | return env, res.CodeSuccess 50 | } 51 | -------------------------------------------------------------------------------- /server/logic/messageLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/17 12:30 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : messageLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | "QLPanelTools/tools/email" 13 | res "QLPanelTools/tools/response" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | // GetEmailData 获取邮件信息 18 | func GetEmailData() (res.ResCode, model.Email) { 19 | return res.CodeSuccess, sqlite.GetEmailOne() 20 | } 21 | 22 | // TestEmailSend 测试服务发送 23 | func TestEmailSend(p *model.TestEmail) res.ResCode { 24 | // 定义收信人 25 | mailTo := []string{p.TEmail} 26 | err := email.SendMail(mailTo, "青龙Tools", "青龙Tools邮件测试发送") 27 | if err != nil { 28 | zap.L().Error(err.Error()) 29 | return res.CodeServerBusy 30 | } 31 | 32 | return res.CodeSuccess 33 | } 34 | 35 | // UpdateEmailSet 更新邮件服务信息 36 | func UpdateEmailSet(p *model.UpdateEmail) res.ResCode { 37 | // 获取第一条配置信息 38 | emailData := sqlite.GetEmailOne() 39 | if emailData.SendMail == "" && emailData.SendPwd == "" && emailData.SMTPServer == "" { 40 | // 首次修改, 创建 41 | emailData.EnableEmail = p.EnableEmail 42 | emailData.SendName = p.SendName 43 | emailData.SendMail = p.SendMail 44 | emailData.SendPwd = p.SendPwd 45 | emailData.SMTPServer = p.SMTPServer 46 | emailData.SMTPPort = p.SMTPPort 47 | emailData.SendName = p.SendName 48 | sqlite.InsertEmailData(emailData) 49 | } else { 50 | sqlite.UpdateEmailData(p) 51 | } 52 | 53 | return res.CodeSuccess 54 | } 55 | -------------------------------------------------------------------------------- /server/logic/panelLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/7 16:42 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : panelLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | "QLPanelTools/tools/panel" 13 | res "QLPanelTools/tools/response" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | // PanelAdd 面板信息存入数据库 18 | func PanelAdd(p *model.PanelData) res.ResCode { 19 | // 保存进数据库 20 | err := sqlite.InsertPanelData(p) 21 | if err != nil { 22 | zap.L().Error("Error inserting database, err:", zap.Error(err)) 23 | return res.CodeStorageFailed 24 | } 25 | 26 | return res.CodeSuccess 27 | } 28 | 29 | // PanelUpdate 更新面板信息 30 | func PanelUpdate(p *model.UpPanelData) res.ResCode { 31 | // 更新数据库 32 | sqlite.UpdatePanelData(p) 33 | return res.CodeSuccess 34 | } 35 | 36 | // PanelDelete 删除面板信息 37 | func PanelDelete(p *model.DelPanelData) res.ResCode { 38 | // 删除面板信息 39 | sqlite.DelPanelData(p) 40 | return res.CodeSuccess 41 | } 42 | 43 | // GetAllPanelData 获取面板全部信息 44 | func GetAllPanelData() ([]model.QLPanel, res.ResCode) { 45 | // 获取信息 46 | panel := sqlite.GetPanelAllData() 47 | return panel, res.CodeSuccess 48 | } 49 | 50 | // UnbindPanelEnvData 解除所有面板变量绑定 51 | func UnbindPanelEnvData() res.ResCode { 52 | // 获取所有面板信息 53 | panel := sqlite.GetPanelAllData() 54 | for i := 0; i < len(panel); i++ { 55 | panel[i].EnvBinding = "" 56 | // 保存数据 57 | sqlite.UnbindPanelEnvData(panel[i]) 58 | } 59 | 60 | return res.CodeSuccess 61 | } 62 | 63 | // UpdatePanelEnvData 修改面板绑定变量 64 | func UpdatePanelEnvData(p *model.PanelEnvData) res.ResCode { 65 | sqlite.UpdatePanelEnvData(p) 66 | return res.CodeSuccess 67 | } 68 | 69 | // UpdateAllPanelToken 解除所有面板变量绑定 70 | func UpdateAllPanelToken() res.ResCode { 71 | // 获取所有面板信息 72 | panelData := sqlite.GetPanelAllData() 73 | for i := 0; i < len(panelData); i++ { 74 | // 更新Token 75 | go panel.GetPanelToken(panelData[i].URL, panelData[i].ClientID, panelData[i].ClientSecret) 76 | } 77 | 78 | return res.CodeSuccess 79 | } 80 | -------------------------------------------------------------------------------- /server/logic/settingLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/21 19:56 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : settingLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | res "QLPanelTools/tools/response" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | // GetSetting 获取一个配置信息 17 | func GetSetting(name string) (model.WebSettings, res.ResCode) { 18 | data, err := sqlite.GetSetting(name) 19 | if err != nil { 20 | zap.L().Error(err.Error()) 21 | return data, res.CodeServerBusy 22 | } 23 | 24 | // 限制前端只能获取公告信息 25 | if name == "notice" { 26 | return data, res.CodeSuccess 27 | } else if name == "backgroundImage" { 28 | return data, res.CodeSuccess 29 | } else if name == "webTitle" { 30 | return data, res.CodeSuccess 31 | } else { 32 | return data, res.CodeServerBusy 33 | } 34 | } 35 | 36 | // GetSettings 获取所有配置信息 37 | func GetSettings() (interface{}, res.ResCode) { 38 | data, err := sqlite.GetSettings() 39 | if err != nil { 40 | zap.L().Error(err.Error()) 41 | return nil, res.CodeServerBusy 42 | } 43 | 44 | return data, res.CodeSuccess 45 | } 46 | 47 | // SaveSettings 保存网站信息 48 | func SaveSettings(p *[]model.WebSettings) res.ResCode { 49 | if err := sqlite.SaveSettings(p); err != nil { 50 | zap.L().Error(err.Error()) 51 | return res.CodeServerBusy 52 | } 53 | 54 | return res.CodeSuccess 55 | } 56 | -------------------------------------------------------------------------------- /server/logic/systemLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/23 15:33 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : systemLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | _const "QLPanelTools/const" 11 | "QLPanelTools/server/model" 12 | "QLPanelTools/server/sqlite" 13 | "QLPanelTools/tools/requests" 14 | res "QLPanelTools/tools/response" 15 | "encoding/json" 16 | "github.com/staktrace/go-update" 17 | "go.uber.org/zap" 18 | "runtime" 19 | ) 20 | 21 | // CheckVersion 检查版本更新 22 | func CheckVersion() (model.WebVer, res.ResCode) { 23 | // 版本号 24 | var v model.Ver 25 | var w model.WebVer 26 | // 获取仓库版本信息 27 | url := "https://version.6b7.xyz/qltools_version.json" 28 | r, err := requests.Requests("GET", url, "", "") 29 | if err != nil { 30 | return w, res.CodeServerBusy 31 | } 32 | // 序列化内容 33 | err = json.Unmarshal(r, &v) 34 | if err != nil { 35 | zap.L().Error(err.Error()) 36 | return w, res.CodeServerBusy 37 | } 38 | if v.Version != _const.Version { 39 | w.Update = true 40 | } else { 41 | w.Update = false 42 | } 43 | w.Notice = v.Notice 44 | w.Version = _const.Version 45 | 46 | return w, res.CodeSuccess 47 | } 48 | 49 | // UpdateSoftware 更新软件 50 | func UpdateSoftware(p *model.SoftWareGOOS) (res.ResCode, string) { 51 | if runtime.GOOS == "windows" { 52 | return res.CodeUpdateServerBusy, "Windows系统不支持此功能" 53 | } 54 | // 获取版本号 55 | var v model.Ver 56 | url := "https://version.6b7.xyz/qltools_version.json" 57 | r, _ := requests.Requests("GET", url, "", "") 58 | _ = json.Unmarshal(r, &v) 59 | if v.Version == _const.Version { 60 | return res.CodeUpdateServerBusy, "已经是最新版本" 61 | } 62 | 63 | // 更新程序 64 | go UpdateSoftWare(v.Version, p.Framework) 65 | 66 | return res.CodeSuccess, "程序已进入自动更新任务,如果更新失败请手动更新" 67 | } 68 | 69 | func UpdateSoftWare(version, GOOS string) { 70 | // 获取代理下载地址 71 | gh, _ := sqlite.GetSetting("ghProxy") 72 | 73 | var url string 74 | if gh.Value != "" { 75 | url = AddStringHTTP(gh.Value) + "https://github.com/nuanxinqing123/QLTools/releases/download/" + version 76 | } else { 77 | url = "https://github.com/nuanxinqing123/QLTools/releases/download/" + version 78 | } 79 | 80 | if GOOS == "amd64" { 81 | url += "/QLTools-linux-amd64" 82 | } else if GOOS == "arm64" { 83 | url += "/QLTools-linux-arm64" 84 | } else { 85 | url += "/QLTools-linux-arm" 86 | } 87 | zap.L().Debug("Download: " + url) 88 | 89 | err := doUpdate(url) 90 | if err != nil { 91 | zap.L().Error(err.Error()) 92 | } 93 | } 94 | 95 | func doUpdate(url string) error { 96 | resp, err := requests.Down(url) 97 | if err != nil { 98 | return err 99 | } 100 | defer resp.Body.Close() 101 | err = update.Apply(resp.Body, update.Options{}) 102 | if err != nil { 103 | // error handling 104 | return err 105 | } 106 | return nil 107 | } 108 | 109 | // AddStringHTTP 处理URL地址结尾的斜杠 110 | func AddStringHTTP(url string) string { 111 | if len(url) == 0 { 112 | return url 113 | } 114 | s := []byte(url) 115 | if s[len(s)-1] != '/' { 116 | url += "/" 117 | } 118 | return url 119 | } 120 | -------------------------------------------------------------------------------- /server/logic/userLogic.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:18 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : userLogic.go 6 | 7 | package logic 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | "QLPanelTools/tools/email" 13 | "QLPanelTools/tools/jwt" 14 | "QLPanelTools/tools/md5" 15 | "QLPanelTools/tools/requests" 16 | res "QLPanelTools/tools/response" 17 | "QLPanelTools/tools/snowflake" 18 | "QLPanelTools/tools/timeTools" 19 | "encoding/json" 20 | "go.uber.org/zap" 21 | "strconv" 22 | "time" 23 | ) 24 | 25 | // SignUp 注册业务 26 | func SignUp(p *model.UserSignUp) res.ResCode { 27 | // 判断是否已存在账户 28 | result, _ := sqlite.GetUserData() 29 | if result == true { 30 | return res.CodeRegistrationClosed 31 | } 32 | 33 | // 密码加密 34 | p.Password = md5.AddMD5(p.Password) 35 | 36 | // 生成用户UID 37 | userID := snowflake.GenID() 38 | 39 | // 构造User实例 40 | user := &model.User{ 41 | UserID: userID, 42 | Email: p.Email, 43 | Username: p.Username, 44 | Password: p.Password, 45 | } 46 | 47 | // 保存进数据库 48 | err := sqlite.InsertUser(user) 49 | if err != nil { 50 | zap.L().Error("Error inserting database, err:", zap.Error(err)) 51 | return res.CodeServerBusy 52 | } 53 | return res.CodeSuccess 54 | } 55 | 56 | // SignIn 登录业务 57 | func SignIn(p *model.UserSignIn) (string, res.ResCode) { 58 | // 检查邮箱格式是否正确 59 | bol := email.VerifyEmailFormat(p.Email) 60 | if bol == false { 61 | return "", res.CodeEmailFormatError 62 | } else { 63 | // 检查邮箱是否存在 64 | code := sqlite.CheckEmail(p.Email) 65 | if code == res.CodeEmailNotExist { 66 | // 不存在 67 | return "", res.CodeEmailNotExist 68 | } else { 69 | // 邮箱存在,记录传入密码 70 | oPassword := p.Password 71 | 72 | // 获取数据库用户信息 73 | _, user := sqlite.GetUserData() 74 | 75 | // 判断密码是否正确 76 | if user.Password != md5.AddMD5(oPassword) { 77 | return "", res.CodeInvalidPassword 78 | } else { 79 | // 密码正确, 返回生成的Token 80 | token, err := jwt.GenToken(user.UserID, user.Email) 81 | if err != nil { 82 | zap.L().Error("An error occurred in token generation, err:", zap.Error(err)) 83 | return "", res.CodeServerBusy 84 | } 85 | return token, res.CodeSuccess 86 | } 87 | } 88 | } 89 | } 90 | 91 | // RePwd 修改密码业务 92 | func RePwd(p *model.ReAdminPwd) (bool, res.ResCode) { 93 | // 获取数据库用户信息 94 | _, user := sqlite.GetUserData() 95 | 96 | // 判断密码是否正确 97 | if user.Password != md5.AddMD5(p.OldPassword) { 98 | return false, res.CodeOldPassWordError 99 | } else { 100 | // 储存新密码 101 | err := sqlite.UpdateUserData(p.Email, md5.AddMD5(p.Password)) 102 | if err != nil { 103 | return false, res.CodeServerBusy 104 | } 105 | return true, res.CodeSuccess 106 | } 107 | 108 | } 109 | 110 | // CheckToken 检查Token是否有效 111 | func CheckToken(p *model.CheckToken) (bool, res.ResCode) { 112 | // 获取管理员信息 113 | _, aData := sqlite.GetUserData() 114 | 115 | // 解析Token 116 | myClaims, err := jwt.ParseToken(p.JWToken) 117 | if err != nil { 118 | return false, res.CodeServerBusy 119 | } 120 | 121 | if aData.Email != myClaims.Email { 122 | return false, res.CodeInvalidToken 123 | } 124 | if aData.UserID != myClaims.UserID { 125 | return false, res.CodeInvalidToken 126 | } 127 | 128 | zap.L().Debug(strconv.FormatInt(myClaims.UserID, 10)) 129 | 130 | return true, res.CodeSuccess 131 | } 132 | 133 | // AddIPAddr 记录登录信息 134 | func AddIPAddr(ip string, ifok bool) { 135 | // 查询IP地址 136 | url := "https://ip.useragentinfo.com/sp/TZb2y?ip=" + ip 137 | addr, err := requests.Requests("GET", url, "", "") 138 | if err != nil { 139 | return 140 | } 141 | 142 | // 序列化 143 | type location struct { 144 | Country string `json:"country"` 145 | ShortName string `json:"short_name"` 146 | Province string `json:"province"` 147 | City string `json:"city"` 148 | Area string `json:"area"` 149 | Isp string `json:"isp"` 150 | Net string `json:"net"` 151 | Ip string `json:"ip"` 152 | Code int `json:"code"` 153 | Desc string `json:"desc"` 154 | } 155 | var l location 156 | // 数据绑定 157 | err = json.Unmarshal(addr, &l) 158 | if err != nil { 159 | zap.L().Error(err.Error()) 160 | } 161 | 162 | ipCreate := &model.LoginRecord{ 163 | LoginDay: timeTools.SwitchTimeStampToDataYear(time.Now().Unix()), 164 | LoginTime: timeTools.SwitchTimeStampToData(time.Now().Unix()), 165 | IP: ip, 166 | IPAddress: l.Country + l.Province + l.City + " | " + l.Isp, 167 | IfOK: ifok, 168 | } 169 | // 储存记录 170 | sqlite.InsertLoginRecord(ipCreate) 171 | go CheckSafeMsg(ip) 172 | } 173 | 174 | // CheckSafeMsg 检查是否触发安全推送 175 | func CheckSafeMsg(ip string) { 176 | // 获取邮件服务器信息 177 | es := sqlite.GetEmailOne() 178 | 179 | // 检查是否开启消息推送 180 | if es.SendMail == "" && es.SendPwd == "" && es.SMTPServer == "" || es.EnableEmail == false { 181 | // 未开启 182 | return 183 | } else { 184 | // 近十条IP登录数据 185 | IPTenData := sqlite.GetFailLoginIPData() 186 | count := 0 187 | // 查询此IP登录失败次数 188 | for i := 0; i < len(IPTenData); i++ { 189 | if IPTenData[i].IP == ip { 190 | if IPTenData[i].IfOK == false { 191 | count++ 192 | } 193 | } 194 | } 195 | // 触发安全推送 196 | if count >= 3 { 197 | zap.L().Debug("触发安全推送") 198 | _, info := sqlite.GetUserData() 199 | mailTo := []string{info.Email} 200 | _ = email.SendMail( 201 | mailTo, 202 | "青龙Tools - 安全推送", 203 | "IP地址:"+ip+",多次失败登录。疑似密码爆破,请管理员尽快处理") 204 | } 205 | } 206 | } 207 | 208 | // GetIPInfo 查询近十条记录 209 | func GetIPInfo() ([]model.IpData, res.ResCode) { 210 | // 查询记录 211 | ip := sqlite.GetIPData() 212 | return ip, res.CodeSuccess 213 | } 214 | 215 | // GetAdminInfo 获取管理员信息 216 | func GetAdminInfo() (string, res.ResCode) { 217 | _, info := sqlite.GetUserData() 218 | return info.Username, res.CodeSuccess 219 | } 220 | 221 | // CheckCDK 检查CDK数据 222 | func CheckCDK(p *model.CheckCDK) (res.ResCode, string) { 223 | // 查询CDK是否存在 224 | c := sqlite.GetCDKData(p.CDK) 225 | if c.CdKey == "" { 226 | // CDK查询为空 227 | return res.CodeSuccess, "" 228 | } 229 | 230 | // CDK是否被禁用 231 | if c.State == false { 232 | // CDK已被管理员禁用 233 | return res.CodeSuccess, "您的CDK已被禁用" 234 | } 235 | 236 | if c.AvailableTimes <= 0 { 237 | // 当前CDK使用次数已耗尽 238 | return res.CodeSuccess, "您CDK使用次数已耗尽" 239 | } 240 | 241 | return res.CodeSuccess, "您的CDK剩余使用次数:" + strconv.Itoa(c.AvailableTimes) 242 | } 243 | -------------------------------------------------------------------------------- /server/middleware/auth.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:05 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : auth.go 6 | 7 | package middleware 8 | 9 | import ( 10 | "QLPanelTools/server/sqlite" 11 | "QLPanelTools/tools/jwt" 12 | res "QLPanelTools/tools/response" 13 | "strings" 14 | 15 | "github.com/gin-gonic/gin" 16 | ) 17 | 18 | const CtxUserIDKey = "userID" 19 | 20 | // UserAuth 基于JWT的认证中间件 21 | func UserAuth() gin.HandlerFunc { 22 | return func(c *gin.Context) { 23 | // Token放在Header的Authorization中,并使用Bearer开头 24 | authHeader := c.Request.Header.Get("Authorization") 25 | if authHeader == "" { 26 | res.ResError(c, res.CodeNeedLogin) 27 | c.Abort() 28 | return 29 | } 30 | // 按空格分割 31 | parts := strings.SplitN(authHeader, " ", 2) 32 | if !(len(parts) == 2 && parts[0] == "Bearer") { 33 | res.ResError(c, res.CodeInvalidToken) 34 | c.Abort() 35 | return 36 | } 37 | // parts[1]是获取到的tokenString,我们使用之前定义好的解析JWT的函数来解析它 38 | mc, err := jwt.ParseToken(parts[1]) 39 | if err != nil { 40 | res.ResError(c, res.CodeInvalidToken) 41 | c.Abort() 42 | return 43 | } 44 | 45 | // 检查是否属于管理员 46 | cAdmin := sqlite.CheckAdmin(mc.UserID) 47 | 48 | if cAdmin != true { 49 | c.Abort() 50 | res.ResErrorWithMsg(c, res.CodeInvalidRouterRequested, "无访问权限或认证已过期") 51 | return 52 | } else { 53 | //将当前请求的userID信息保存到请求的上下文c上 54 | c.Set(CtxUserIDKey, mc.UserID) 55 | c.Next() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/middleware/cors.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/7/25 21:00 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : cors.go 6 | 7 | package middleware 8 | 9 | import ( 10 | "github.com/gin-gonic/gin" 11 | "net/http" 12 | ) 13 | 14 | // Cors 配置跨域 15 | func Cors() gin.HandlerFunc { 16 | return func(context *gin.Context) { 17 | method := context.Request.Method 18 | context.Header("Access-Control-Allow-Origin", "*") 19 | context.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token") 20 | context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS") 21 | context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type") 22 | context.Header("Access-Control-Allow-Credentials", "true") 23 | if method == "OPTIONS" { 24 | context.AbortWithStatus(http.StatusNoContent) 25 | } 26 | context.Next() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/middleware/limiter.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:46 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : limiter.go 6 | 7 | package middleware 8 | 9 | import ( 10 | res "QLPanelTools/tools/response" 11 | "github.com/gin-gonic/gin" 12 | "github.com/juju/ratelimit" 13 | "time" 14 | ) 15 | 16 | // RateLimitMiddleware 限流 17 | func RateLimitMiddleware(fillInterval time.Duration, cap, quantum int64) gin.HandlerFunc { 18 | bucket := ratelimit.NewBucketWithQuantum(fillInterval, cap, quantum) 19 | return func(c *gin.Context) { 20 | if bucket.TakeAvailable(1) < 1 { 21 | res.ResErrorWithMsg(c, res.CodeServerBusy, "异常流量") 22 | c.Abort() 23 | return 24 | } 25 | c.Next() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/model/cdkAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/12 14:09 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : cdkAdmin.go 6 | 7 | package model 8 | 9 | import "gorm.io/gorm" 10 | 11 | type CDK struct { 12 | gorm.Model 13 | CdKey string // CD-KEY值 14 | AvailableTimes int // CD-KEY剩余可用次数 15 | State bool // CD-KEY状态(true:启用、false:禁用) 16 | } 17 | 18 | type CreateCDK struct { 19 | CdKeyCount int `json:"cdKeyCount" binding:"required"` // CDK生成数量 20 | CdKeyAvailableTimes int `json:"cdKeyAvailableTimes" binding:"required"` // CDK使用次数 21 | } 22 | 23 | type UpdateStateCDK struct { 24 | State int `json:"state" binding:"required"` // CKD状态(1:启用、2:禁用) 25 | } 26 | 27 | type UpdateCDK struct { 28 | ID int `json:"id" binding:"required"` // CDK ID 29 | AvailableTimes int `json:"availableTimes"` // CDK使用次数 30 | State bool `json:"state"` // CKD状态 31 | } 32 | 33 | type DelCDK struct { 34 | ID int `json:"id" binding:"required"` // CDK ID 35 | } 36 | 37 | type CDKPageData struct { 38 | Page int64 `json:"page"` // 总页数 39 | CDKData []CDK `json:"CDKData"` // 分页查询CDK数据 40 | } 41 | 42 | type CheckCDK struct { 43 | CDK string `json:"CDK" binding:"required"` 44 | } 45 | -------------------------------------------------------------------------------- /server/model/conAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/24 19:33 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : conAdmin.go 6 | 7 | package model 8 | 9 | import ( 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // OperationRecord 日志记录数据表 14 | type OperationRecord struct { 15 | gorm.Model 16 | OccurrenceTime string // 发生时间 17 | Operation string // 操作方式 18 | Journal string // 记录日志 19 | } 20 | 21 | // TransferM 容器:迁移数据 22 | type TransferM struct { 23 | IDOne int `json:"IDOne"` 24 | IDTwo int `json:"IDTwo"` 25 | } 26 | 27 | // CopyM 容器:复制数据 28 | type CopyM struct { 29 | IDOne int `json:"IDOne"` 30 | IDTwo int `json:"IDTwo"` 31 | } 32 | 33 | // BackupM 容器:备份数据 34 | type BackupM struct { 35 | IDOne int `json:"IDOne"` 36 | } 37 | 38 | // PanelAllEnv 面板全部变量数据 39 | type PanelAllEnv struct { 40 | Code int `json:"code"` 41 | Data []AllEnv `json:"data"` 42 | } 43 | 44 | type AllEnv struct { 45 | ID int `json:"ID"` 46 | Name string `json:"name"` 47 | Value string `json:"value"` 48 | Remarks string `json:"remarks"` 49 | } 50 | -------------------------------------------------------------------------------- /server/model/envAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/6 16:45 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : envAdmin.go 6 | 7 | package model 8 | 9 | import "gorm.io/gorm" 10 | 11 | // EnvName 变量名 12 | type EnvName struct { 13 | gorm.Model 14 | Name string // 环境变量名称 15 | NameRemarks string // 环境变量名称备注 16 | Quantity int // 环境变量数量上限 17 | Regex string // 环境变量匹配正则 18 | Mode int // 环境变量模式[1:新建模式、2:合并模式、3、更新模式] 19 | Division string // 环境变量分隔符(合并模式) 20 | ReUpdate string // 环境变量更新匹配正则(更新模式) 21 | IsPlugin bool // 环境变量是否使用插件 22 | PluginName string // 绑定的插件名称 23 | IsCDK bool // 环境变量是否绑定CDK 24 | } 25 | 26 | // EnvNameAdd 新增变量名 27 | type EnvNameAdd struct { 28 | EnvName string `json:"envName" binding:"required"` 29 | EnvNameRemarks string `json:"envNameRemarks"` 30 | EnvQuantity int `json:"envQuantity" binding:"required"` 31 | EnvRegex string `json:"envRegex"` 32 | EnvMode int `json:"envMode" binding:"required"` 33 | EnvDivision string `json:"envDivision"` 34 | EnvReUpdate string `json:"envReUpdate"` 35 | EnvIsPlugin bool `json:"envIsPlugin"` 36 | EnvPluginName string `json:"envPluginName"` 37 | EnvIsCDK bool `json:"envIsCDK"` 38 | } 39 | 40 | // EnvNameUp 修改变量名 41 | type EnvNameUp struct { 42 | EnvID int `json:"envID" binding:"required"` 43 | EnvName string `json:"envName" binding:"required"` 44 | EnvNameRemarks string `json:"envNameRemarks"` 45 | EnvQuantity int `json:"envQuantity" binding:"required"` 46 | EnvRegex string `json:"envRegex"` 47 | EnvMode int `json:"envMode" binding:"required"` 48 | EnvDivision string `json:"envDivision"` 49 | EnvReUpdate string `json:"envReUpdate"` 50 | EnvIsPlugin bool `json:"envIsPlugin"` 51 | EnvPluginName string `json:"envPluginName"` 52 | EnvIsCDK bool `json:"envIsCDK"` 53 | } 54 | 55 | // EnvNameDel 删除变量名 56 | type EnvNameDel struct { 57 | EnvID int `json:"envID" binding:"required"` 58 | } 59 | 60 | // envNameData 变量数据 61 | type envNameData struct { 62 | // 变量名称 63 | Name string `json:"name"` 64 | // 变量备注 65 | NameRemarks string `json:"nameRemarks"` 66 | // 变量剩余限额 67 | Quantity int `json:"quantity"` 68 | } 69 | 70 | // EnvAdd 上传变量 71 | type EnvAdd struct { 72 | // 服务器ID 73 | ServerID int `json:"serverID" binding:"required"` 74 | // 变量名 75 | EnvName string `json:"envName" binding:"required"` 76 | // 变量值 77 | EnvData string `json:"envData" binding:"required"` 78 | // 备注 79 | EnvRemarks string `json:"envRemarks"` 80 | // CDK 81 | EnvCDK string `json:"envCDK"` 82 | } 83 | -------------------------------------------------------------------------------- /server/model/jsAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/6 16:16 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : jsAdmin.go 6 | 7 | package model 8 | 9 | // DeletePlugin 删除插件 10 | type DeletePlugin struct { 11 | FileName string `json:"FileName"` 12 | } 13 | 14 | // FileInfo 读取插件信息 15 | type FileInfo struct { 16 | FileName string `json:"FileName"` 17 | FileIDName string `json:"FileIDName"` 18 | } 19 | -------------------------------------------------------------------------------- /server/model/jwtAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/16 19:13 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : jwtAdmin.go 6 | 7 | package model 8 | 9 | import "gorm.io/gorm" 10 | 11 | // JWTAdmin JWT密钥 12 | type JWTAdmin struct { 13 | gorm.Model 14 | SecretKey string 15 | } 16 | -------------------------------------------------------------------------------- /server/model/msgAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/17 11:30 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : msgAdmin.go 6 | 7 | package model 8 | 9 | import "gorm.io/gorm" 10 | 11 | type Email struct { 12 | gorm.Model 13 | EnableEmail bool // 推送状态 14 | SendMail string // 发件人邮箱 15 | SendPwd string // 发件人密码 16 | SMTPServer string // SMTP 邮件服务器地址 17 | SMTPPort int // SMTP端口 18 | SendName string // 发件人昵称 19 | } 20 | 21 | type UpdateEmail struct { 22 | EnableEmail bool `json:"enableEmail"` // 推送状态 23 | SendMail string `json:"sendMail" binding:"required"` // 发件人邮箱 24 | SendPwd string `json:"sendPwd" binding:"required"` // 发件人密码 25 | SMTPServer string `json:"SMTPServer" binding:"required"` // SMTP 邮件服务器地址 26 | SMTPPort int `json:"SMTPPort" binding:"required"` // SMTP端口 27 | SendName string `json:"sendName" binding:"required"` // 发件人昵称 28 | } 29 | 30 | type TestEmail struct { 31 | TEmail string `json:"TestEmail"` // 测试邮件发送 32 | } 33 | -------------------------------------------------------------------------------- /server/model/panelAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/6 17:45 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : panelAdmin.go 6 | 7 | package model 8 | 9 | import "gorm.io/gorm" 10 | 11 | // QLPanel QL面板数据 12 | type QLPanel struct { 13 | gorm.Model 14 | PanelName string `binding:"required"` // 面板名称 15 | URL string `binding:"required"` // 面板连接地址 16 | ClientID string `binding:"required"` // 面板Client_ID 17 | ClientSecret string `binding:"required"` // 面板Client_Secret 18 | Enable bool `binding:"required"` // 是否启用面板 19 | PanelVersion bool `binding:"required"` // 面板版本(0 - 旧版本 / 1 - 新版本) 20 | Token string // 面板Token 21 | Params int // 面板Params 22 | EnvBinding string // 绑定变量 23 | } 24 | 25 | // PanelAll 全部面板信息 26 | type PanelAll struct { 27 | ID uint `json:"ID"` 28 | PanelName string `json:"name"` 29 | URL string `json:"url"` 30 | ClientID string `json:"id"` 31 | ClientSecret string `json:"secret"` 32 | Enable bool `json:"enablePanel"` // 是否启用面板 33 | PanelVersion bool `json:"panelVersion"` // 面板版本(0 - 新版本 / 1 - 旧版本) 34 | EnvBinding string `json:"envBinding"` 35 | } 36 | 37 | // PanelData 创建面板数据 38 | type PanelData struct { 39 | Name string `json:"name"` // 面板名称 40 | URL string `json:"url" binding:"required"` // 面板连接地址 41 | ID string `json:"id" binding:"required"` // 面板Client_ID 42 | Secret string `json:"secret" binding:"required"` // 面板Client_Secret 43 | Enable bool `json:"enablePanel"` // 是否启用面板 44 | PanelVersion bool `json:"panelVersion"` // 面板版本(0 - 新版本 / 1 - 旧版本) 45 | } 46 | 47 | // UpPanelData 更新面板数据 48 | type UpPanelData struct { 49 | UID int `json:"uid" binding:"required"` // 数据库ID值 50 | Name string `json:"name" binding:"required"` // 面板名称 51 | URL string `json:"url" binding:"required"` // 面板连接地址 52 | ID string `json:"id" binding:"required"` // 面板Client_ID 53 | Secret string `json:"secret" binding:"required"` // 面板Client_Secret 54 | Enable bool `json:"enablePanel"` // 是否启用面板 55 | PanelVersion bool `json:"panelVersion"` // 面板版本(0 - 新版本 / 1 - 旧版本) 56 | } 57 | 58 | // DelPanelData 删除面板数据 59 | type DelPanelData struct { 60 | UID int `json:"uid" binding:"required"` // 数据库ID值 61 | } 62 | 63 | // PanelEnvData 修改面板绑定变量 64 | type PanelEnvData struct { 65 | UID int `json:"uid" binding:"required"` // 数据库ID值 66 | EnvBinding []string `json:"envBinding" binding:"required"` // 变量值 67 | } 68 | 69 | type EnvStartServer struct { 70 | // 可用服务器组 71 | ServerData []envSData `json:"serverData"` 72 | } 73 | 74 | type envSData struct { 75 | // 容器ID 76 | ID int `json:"ID"` 77 | // 容器名称 78 | Name string `json:"PanelName"` 79 | // 容器绑定变量 80 | EnvData []envNameData `json:"envData"` 81 | } 82 | 83 | // Token 面板Token数据 84 | type Token struct { 85 | Code int `json:"code"` 86 | Data struct { 87 | Token string `json:"token"` 88 | TokenType string `json:"token_type"` 89 | Expiration int `json:"expiration"` 90 | } `json:"data"` 91 | Message string 92 | } 93 | 94 | // PanelRes 面板Token数据 95 | type PanelRes struct { 96 | Code int `json:"code"` 97 | StatusCode int `json:"statusCode"` 98 | Message string 99 | } 100 | 101 | // EnvData 面板变量数据 102 | type EnvData struct { 103 | Code int `json:"code"` 104 | Data []envData `json:"data"` 105 | } 106 | 107 | type envData struct { 108 | ID int `json:"id"` 109 | OId string `json:"_id"` 110 | Name string `json:"name"` 111 | Value string `json:"value"` 112 | Remarks string `json:"remarks"` 113 | } 114 | -------------------------------------------------------------------------------- /server/model/settingAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/21 19:48 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : settingAdmin.go 6 | 7 | package model 8 | 9 | import "gorm.io/gorm" 10 | 11 | // WebSettings 网站配置模型 12 | type WebSettings struct { 13 | Key string `json:"key" gorm:"primaryKey" binding:"required"` 14 | Value string `json:"value"` 15 | } 16 | 17 | // IPSubmitRecord IP提交记录模型 18 | type IPSubmitRecord struct { 19 | gorm.Model 20 | SubmitTime string `json:"submit_time" binding:"required"` // 提交时间(格式:2022-04-17) 21 | IPAddress string `json:"ip_address" binding:"required"` // 提交IP 22 | } 23 | -------------------------------------------------------------------------------- /server/model/sysAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/23 16:00 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : sysAdmin.go 6 | 7 | package model 8 | 9 | type Ver struct { 10 | Version string `json:"Version"` 11 | Notice string `json:"Notice"` 12 | } 13 | 14 | type WebVer struct { 15 | Update bool `json:"Update"` 16 | Version string `json:"Version"` 17 | Notice string `json:"Notice"` 18 | } 19 | 20 | type SoftWareGOOS struct { 21 | Framework string `json:"framework" binding:"required"` 22 | } 23 | -------------------------------------------------------------------------------- /server/model/userAdmin.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:07 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : userAdmin.go 6 | 7 | package model 8 | 9 | import ( 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // User 用户模型 14 | type User struct { 15 | /* 16 | gorm.Model:基础结构(ID、CreatedAt、UpdatedAt、DeletedAt) 17 | */ 18 | gorm.Model 19 | // 用户ID 20 | UserID int64 `binding:"required"` 21 | // 用户邮箱 22 | Email string `binding:"required"` 23 | // 用户名 24 | Username string `binding:"required"` 25 | // 用户密码 26 | Password string `binding:"required"` 27 | } 28 | 29 | // LoginRecord 登录记录模型 30 | type LoginRecord struct { 31 | gorm.Model 32 | LoginDay string `binding:"required"` 33 | LoginTime string `binding:"required"` 34 | IP string `binding:"required"` 35 | IPAddress string `binding:"required"` 36 | IfOK bool `binding:"required"` 37 | } 38 | 39 | // UserSignUp 用户注册模型 40 | type UserSignUp struct { 41 | Email string `json:"email" binding:"required"` 42 | Username string `json:"username" binding:"required"` 43 | Password string `json:"password" binding:"required"` 44 | RePassword string `json:"re_password" binding:"required,eqfield=Password"` 45 | } 46 | 47 | // UserSignIn 用户登录模型 48 | type UserSignIn struct { 49 | Email string `json:"email" binding:"required"` 50 | Password string `json:"password" binding:"required"` 51 | } 52 | 53 | // ReAdminPwd 修改密码模型 54 | type ReAdminPwd struct { 55 | OldPassword string `json:"old_password" binding:"required"` 56 | Email string `json:"email"` 57 | Password string `json:"password" binding:"required"` 58 | RePassword string `json:"re_password" binding:"required,eqfield=Password"` 59 | } 60 | 61 | // CheckToken 检查Token是否有效 62 | type CheckToken struct { 63 | JWToken string `json:"token" binding:"required"` 64 | } 65 | 66 | // IPModel IP模型 67 | type IPModel struct { 68 | Data []IpData `json:"Data"` 69 | } 70 | 71 | type IpData struct { 72 | LoginTime string `json:"login_time"` 73 | IP string `json:"ip"` 74 | IPAddress string `json:"ip_address"` 75 | IfOK bool `json:"if_ok"` 76 | } 77 | -------------------------------------------------------------------------------- /server/routes.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 13:18 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : routes.go 6 | 7 | package routes 8 | 9 | import ( 10 | "QLPanelTools/server/controllers" 11 | "QLPanelTools/server/middleware" 12 | "QLPanelTools/static/bindata" 13 | "QLPanelTools/tools/logger" 14 | "html/template" 15 | "strings" 16 | "time" 17 | 18 | assetfs "github.com/elazarl/go-bindata-assetfs" 19 | 20 | "github.com/gin-gonic/gin" 21 | "github.com/spf13/viper" 22 | ) 23 | 24 | func Setup() *gin.Engine { 25 | // 创建服务 26 | r := gin.Default() 27 | 28 | // 配置中间件 29 | { 30 | // 配置日志 31 | if viper.GetString("app.mode") == "" { 32 | r.Use(logger.GinLogger(), logger.GinRecovery(true)) 33 | } 34 | } 35 | 36 | // 前端静态文件 37 | { 38 | // 加载模板文件 39 | t, err := loadTemplate() 40 | if err != nil { 41 | panic(err) 42 | } 43 | r.SetHTMLTemplate(t) 44 | 45 | // 加载静态文件 46 | fs := assetfs.AssetFS{ 47 | Asset: bindata.Asset, 48 | AssetDir: bindata.AssetDir, 49 | AssetInfo: nil, 50 | Prefix: "assets", 51 | } 52 | r.StaticFS("/static", &fs) 53 | 54 | r.GET("/", func(c *gin.Context) { 55 | c.HTML(200, "index.html", nil) 56 | }) 57 | } 58 | 59 | // 路由组 60 | { 61 | // 开放权限组 62 | open := r.Group("v1/api") 63 | open.Use(middleware.Cors()) // 允许跨域 64 | { 65 | // 账户注册 66 | open.POST("signup", controllers.SignUpHandle) 67 | // 账户登录 68 | open.POST("signin", middleware.RateLimitMiddleware(time.Minute, 5, 5), controllers.SignInHandle) // 每分钟限制5次请求, 超出熔断 69 | // 检查Token是否有效 70 | open.POST("check/token", controllers.CheckToken) 71 | // 设置:获取单个配置 72 | open.GET("set/setting", controllers.GetSetting) 73 | // CDK状态检查 74 | open.POST("check/cdk", controllers.CheckCDK) 75 | 76 | // 可用服务 77 | open.GET("index/data", controllers.IndexData) 78 | // 上传变量 79 | open.POST("env/add", controllers.EnvADD) 80 | } 81 | 82 | // 管理员权限组 83 | ad := r.Group("v2/api") 84 | ad.Use(middleware.UserAuth()) 85 | { 86 | // 测试 87 | ad.GET("123", controllers.AdminTest) 88 | // 面板连接测试 89 | ad.POST("panel/connect", controllers.GetPanelToken) 90 | 91 | // 管理员:获取前十次登录记录 92 | ad.GET("admin/ip/info", controllers.GetIPInfo) 93 | // 管理员:密码修改 94 | ad.POST("admin/rep-wd", controllers.ReAdminPwd) 95 | // 管理员: 获取管理员信息 96 | ad.GET("admin/info", controllers.GetAdminInfo) 97 | 98 | // 变量名:新增 99 | ad.POST("env/name/add", controllers.EnvNameAdd) 100 | // 变量名:修改 101 | ad.PUT("env/name/update", controllers.EnvNameUp) 102 | // 变量名:删除 103 | ad.DELETE("env/name/del", controllers.EnvNameDel) 104 | // 变量名:All 105 | ad.GET("env/name/all", controllers.GetAllEnvData) 106 | 107 | // 面板:新增 108 | ad.POST("env/panel/add", controllers.PanelAdd) 109 | // 面板:修改 110 | ad.PUT("env/panel/update", controllers.PanelUp) 111 | // 面板:删除 112 | ad.DELETE("env/panel/del", controllers.PanelDel) 113 | // 面板:All 114 | ad.GET("env/panel/all", controllers.GetAllPanelData) 115 | // 面板:绑定变量 116 | ad.PUT("env/panel/binding/update", controllers.UpdatePanelEnvData) 117 | // 面板:解除所有面板变量绑定 118 | ad.PUT("env/panel/unbind/update", controllers.UnbindPanelEnvData) 119 | // 面板:批量更新面板Token 120 | ad.PUT("env/panel/token/update", controllers.UpdateAllPanelToken) 121 | 122 | // 容器:迁移 123 | ad.POST("container/transfer", controllers.Transfer) 124 | // 容器:复制 125 | ad.POST("container/copy", controllers.Copy) 126 | // 容器:备份 127 | ad.POST("container/backup", controllers.Backup) 128 | // 容器:恢复 129 | ad.POST("container/restore", controllers.Restore) 130 | // 容器:十条记录 131 | ad.GET("container/info", controllers.Info) 132 | // 容器:下载备份数据 133 | ad.POST("container/backup/data", controllers.BackupDownload) 134 | 135 | // 插件:上传插件 136 | ad.POST("javascript/upload", controllers.JavascriptUpload) 137 | // 插件:删除插件 138 | ad.POST("javascript/delete", controllers.JavascriptDelete) 139 | // 插件:读取plugin目录下所有插件 140 | ad.GET("javascript/readall", controllers.JavascriptReadall) 141 | 142 | // CDK:以20条数据分割 143 | ad.GET("cd-key/division/data", controllers.GetDivisionCDKData) 144 | // CDK:获取全部信息/启用/禁用 145 | ad.GET("cd-key/data", controllers.GetCDKData) 146 | // CDK:批量新增 147 | ad.POST("cd-key/add", controllers.CreateCDKData) 148 | // CDK:下载生成CDK文件 149 | ad.GET("cd-key/data/download", controllers.DownloadCDKData) 150 | // CDK:全部启用/禁用 151 | ad.PUT("cd-key/all/state", controllers.CDKState) 152 | // CDK:修改 153 | ad.PUT("cd-key/update", controllers.UpdateCDK) 154 | // CDK:删除 155 | ad.DELETE("cd-key/delete", controllers.DelCDK) 156 | 157 | // 消息推送: 获取信息 158 | ad.GET("message/data", controllers.GetEmailData) 159 | // 消息推送: 测速发送 160 | ad.POST("message/send/test", controllers.SendTestEmail) 161 | // 消息推送: 修改 162 | ad.POST("message/update", controllers.UpdateEmailSet) 163 | 164 | // 设置:获取全部配置 165 | ad.GET("set/settings", controllers.GetSettings) 166 | // 设置:修改网站配置信息 167 | ad.PUT("set/settings", controllers.SaveSettings) 168 | 169 | // 系统:检查版本 170 | ad.GET("check/version", controllers.CheckVersion) 171 | // 系统:更新程序 172 | ad.POST("check/update/software", controllers.UpdateSoftware) 173 | } 174 | } 175 | 176 | return r 177 | } 178 | 179 | // loadTemplate 加载模板文件 180 | func loadTemplate() (*template.Template, error) { 181 | t := template.New("") 182 | for _, name := range bindata.AssetNames() { 183 | if !strings.HasSuffix(name, ".html") { 184 | continue 185 | } 186 | asset, err := bindata.Asset(name) 187 | if err != nil { 188 | continue 189 | } 190 | name := strings.Replace(name, "assets/", "", 1) 191 | t, err = t.New(name).Parse(string(asset)) 192 | if err != nil { 193 | return nil, err 194 | } 195 | } 196 | return t, nil 197 | } 198 | -------------------------------------------------------------------------------- /server/settings/settings.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 12:55 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : settings.go 6 | 7 | package settings 8 | 9 | import ( 10 | "fmt" 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | type App struct { 16 | // 运行模式 17 | Mode string `mapstructure:"mode"` 18 | // 运行端口 19 | Port int `mapstructure:"port"` 20 | } 21 | 22 | // Conf 保存配置信息 23 | var Conf = new(App) 24 | 25 | func Init() (err error) { 26 | // 配置文件名称 27 | viper.SetConfigName("config") 28 | // 配置文件扩展名 29 | viper.SetConfigType("yaml") 30 | // 配置文件所在路径 31 | viper.AddConfigPath("./config") 32 | // 查找并读取配置文件 33 | err = viper.ReadInConfig() 34 | if err != nil { 35 | // 处理读取配置文件的错误 36 | fmt.Printf("viper.ReadInConfig() failed, err:%v\n", err) 37 | return 38 | } 39 | 40 | // 配置信息绑定到结构体变量 41 | err = viper.Unmarshal(Conf) 42 | if err != nil { 43 | fmt.Printf("viper.Unmarshal() failed, err:%v\n", err) 44 | } 45 | 46 | // 热加载配置文件 47 | viper.WatchConfig() 48 | viper.OnConfigChange(func(c fsnotify.Event) { 49 | fmt.Println("检测到配置文件有变动,已实时加载") 50 | }) 51 | return 52 | } 53 | -------------------------------------------------------------------------------- /server/sqlite/cdkSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/12 14:37 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : cdkSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "go.uber.org/zap" 12 | "strconv" 13 | ) 14 | 15 | // GetDivisionCDKData 条件查询CDK数据 16 | func GetDivisionCDKData(page int) []model.CDK { 17 | var cdk []model.CDK 18 | if page == 1 { 19 | // 获取前20条数据 20 | sql := "select * from cdks where `deleted_at` IS NULL limit 0, " + strconv.Itoa(19) + ";" 21 | zap.L().Debug(sql) 22 | DB.Raw(sql).Scan(&cdk) 23 | } else { 24 | /* 25 | 1、获取指定页数的数据 26 | 2、20条数据为一页 27 | 3、起始位置:((page - 1) * 20) - 1 28 | 4、结束位置:(page * 20) - 1 29 | */ 30 | sql := "select * from cdks where `deleted_at` IS NULL limit " + strconv.Itoa(((page-1)*20)-1) + ", " + strconv.Itoa((page*20)-1) + ";" 31 | zap.L().Debug(sql) 32 | DB.Raw(sql).Scan(&cdk) 33 | } 34 | return cdk 35 | } 36 | 37 | // GetCDKDataPage 获取CDK表总数据 38 | func GetCDKDataPage() int64 { 39 | var c []model.CDK 40 | result := DB.Find(&c) 41 | return result.RowsAffected 42 | } 43 | 44 | // GetAllCDKData 查询全部CDK数据 45 | func GetAllCDKData() []model.CDK { 46 | var c []model.CDK 47 | DB.Find(&c) 48 | return c 49 | } 50 | 51 | // GetTrueCDKData 查询启用CDK数据 52 | func GetTrueCDKData() []model.CDK { 53 | var c []model.CDK 54 | DB.Where("state = ?", true).Find(&c) 55 | return c 56 | } 57 | 58 | // GetFalseCDKData 查询禁用CDK数据 59 | func GetFalseCDKData() []model.CDK { 60 | var c []model.CDK 61 | DB.Where("state = ?", false).Find(&c) 62 | return c 63 | } 64 | 65 | // GetOneCDKData 查询指定CDK数据 66 | func GetOneCDKData(cdk string) []model.CDK { 67 | var c []model.CDK 68 | DB.Where("cd_key = ?", cdk).First(&c) 69 | return c 70 | } 71 | 72 | // InsertCDKData 生成CDK写入数据库 73 | func InsertCDKData(p *model.CDK) { 74 | var cdk model.CDK 75 | cdk.CdKey = p.CdKey 76 | cdk.AvailableTimes = p.AvailableTimes 77 | cdk.State = p.State 78 | DB.Create(&cdk) 79 | } 80 | 81 | // UpdateCDKDataState 批量更新CDK状态 82 | func UpdateCDKDataState(p *model.UpdateStateCDK) { 83 | cdk := new(model.CDK) 84 | if p.State == 1 { 85 | // 启用 86 | zap.L().Debug("CDK全部:启用") 87 | DB.Model(&cdk).Where("state = ?", false).Update("state", true) 88 | } else { 89 | // 禁用 90 | zap.L().Debug("CDK全部:禁用") 91 | DB.Model(&cdk).Where("state = ?", true).Update("state", false) 92 | } 93 | } 94 | 95 | // UpdateCDKData 更新单条CDK数据 96 | func UpdateCDKData(p *model.UpdateCDK) { 97 | cdk := new(model.CDK) 98 | DB.Where("id = ?", p.ID).First(&cdk) 99 | cdk.AvailableTimes = p.AvailableTimes 100 | cdk.State = p.State 101 | DB.Save(&cdk) 102 | } 103 | 104 | // DelCDKData 删除CDK数据 105 | func DelCDKData(p *model.DelCDK) { 106 | cdk := new(model.CDK) 107 | DB.Where("id = ? ", p.ID).First(&cdk) 108 | DB.Delete(&cdk) 109 | } 110 | 111 | // GetCDKData 查询CDK信息 112 | func GetCDKData(p string) model.CDK { 113 | var c model.CDK 114 | DB.Where("cd_key = ?", p).First(&c) 115 | return c 116 | } 117 | -------------------------------------------------------------------------------- /server/sqlite/containerSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/25 18:20 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : containerSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/tools/timeTools" 12 | "time" 13 | ) 14 | 15 | // RecordingError 记录错误 16 | func RecordingError(journal, info string) { 17 | var er model.OperationRecord 18 | // 发生时间 19 | er.OccurrenceTime = timeTools.SwitchTimeStampToData(time.Now().Unix()) 20 | // 记录日志 21 | er.Journal = info 22 | // 操作方式 23 | er.Operation = journal 24 | DB.Create(&er) 25 | } 26 | 27 | // GetConData 获取十条错误记录 28 | func GetConData() []model.OperationRecord { 29 | var i []model.OperationRecord 30 | sqlStr := "SELECT `occurrence_time`, `journal`, `operation` FROM `operation_records` order by id desc limit 0,10;" 31 | DB.Raw(sqlStr).Scan(&i) 32 | return i 33 | } 34 | -------------------------------------------------------------------------------- /server/sqlite/envSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/6 17:15 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : envSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/tools/timeTools" 12 | "go.uber.org/zap" 13 | "strconv" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // CheckEnvName 检查变量名是否存在 19 | func CheckEnvName(name string) bool { 20 | var env model.EnvName 21 | DB.Where("name = ?", name).First(env) 22 | if env.ID != 0 { 23 | // 变量名已存在 24 | return true 25 | } else { 26 | // 变量名不存在 27 | return false 28 | } 29 | } 30 | 31 | // AddEnvName 新增变量名 32 | func AddEnvName(data *model.EnvNameAdd) (err error) { 33 | var e model.EnvName 34 | e.Name = data.EnvName 35 | e.NameRemarks = data.EnvNameRemarks 36 | e.Quantity = data.EnvQuantity 37 | e.Regex = data.EnvRegex 38 | e.Mode = data.EnvMode 39 | e.Division = data.EnvDivision 40 | e.ReUpdate = data.EnvReUpdate 41 | e.IsPlugin = data.EnvIsPlugin 42 | e.PluginName = data.EnvPluginName 43 | e.IsCDK = data.EnvIsCDK 44 | err = DB.Create(&e).Error 45 | if err != nil { 46 | zap.L().Error("Insert data error, err:", zap.Error(err)) 47 | return 48 | } 49 | return 50 | } 51 | 52 | // UpdateEnvName 更新变量信息 53 | func UpdateEnvName(data *model.EnvNameUp) { 54 | var d model.EnvName 55 | // 通过ID查询并更新数据 56 | DB.Where("id = ?", data.EnvID).First(&d) 57 | d.Name = data.EnvName 58 | d.NameRemarks = data.EnvNameRemarks 59 | d.Quantity = data.EnvQuantity 60 | d.Regex = data.EnvRegex 61 | d.Mode = data.EnvMode 62 | d.Division = data.EnvDivision 63 | d.ReUpdate = data.EnvReUpdate 64 | d.IsPlugin = data.EnvIsPlugin 65 | d.PluginName = data.EnvPluginName 66 | d.IsCDK = data.EnvIsCDK 67 | DB.Save(&d) 68 | } 69 | 70 | // DelEnvName 删除变量信息 71 | func DelEnvName(data *model.EnvNameDel) { 72 | var d model.EnvName 73 | DB.Where("id = ? ", data.EnvID).First(&d) 74 | DB.Delete(&d) 75 | } 76 | 77 | // GetEnvNameAll 获取变量All数据 78 | func GetEnvNameAll() []model.EnvName { 79 | var s []model.EnvName 80 | DB.Find(&s) 81 | return s 82 | } 83 | 84 | // GetEnvAllByID 根据ID值获取变量数据 85 | func GetEnvAllByID(id int) []model.EnvName { 86 | // ID值查询服务 87 | var s model.QLPanel 88 | DB.First(&s, id) 89 | // 转换切片 90 | envBind := strings.Split(s.EnvBinding, "@") 91 | // 切片转换int类型 92 | var e []int 93 | zap.L().Debug("面板为:" + s.PanelName) 94 | for i := 0; i < len(envBind); i++ { 95 | zap.L().Debug("面板绑定变量数据为:" + envBind[i]) 96 | if envBind[i] != "" { 97 | ee, err := strconv.Atoi(envBind[i]) 98 | if err != nil { 99 | zap.L().Error(err.Error()) 100 | } 101 | e = append(e, ee) 102 | } 103 | } 104 | 105 | var env []model.EnvName 106 | // 根据绑定值查询变量数据 107 | if len(e) != 0 { 108 | DB.Find(&env, e) 109 | } 110 | return env 111 | } 112 | 113 | // GetEnvNameCount 根据变量名获取配额 114 | func GetEnvNameCount(name string) int { 115 | var env model.EnvName 116 | DB.Where("name = ?", name).First(&env) 117 | return env.Quantity 118 | } 119 | 120 | // CheckIPCount 查询IP今日已上传次数 121 | func CheckIPCount(ip string, value string) bool { 122 | var p string 123 | todayTime := timeTools.SwitchTimeStampToDataYear(time.Now().Unix()) 124 | sqlStr := "SELECT count(ip_address) FROM `ip_submit_records` WHERE submit_time = '" + todayTime + "' AND ip_address = '" + ip + "'" 125 | DB.Raw(sqlStr).Scan(&p) 126 | 127 | // 判断是否超出限制 128 | a, _ := strconv.Atoi(p) 129 | b, _ := strconv.Atoi(value) 130 | if a >= b { 131 | return true 132 | } else { 133 | return false 134 | } 135 | } 136 | 137 | // InsertSubmitRecord 记录上传IP 138 | func InsertSubmitRecord(ip string) { 139 | var lr model.IPSubmitRecord 140 | lr.SubmitTime = timeTools.SwitchTimeStampToDataYear(time.Now().Unix()) 141 | lr.IPAddress = ip 142 | DB.Create(&lr) 143 | } 144 | 145 | // UpdateCDKAvailableTimes 更新CDK使用次数 146 | func UpdateCDKAvailableTimes(p *model.EnvAdd) { 147 | // 查询CDK 148 | cdk := GetCDKData(p.EnvCDK) 149 | // 查询变量 150 | _, eData := CheckEnvNameDoesItExist(p.EnvName) 151 | if eData.IsCDK != false { 152 | cdk.AvailableTimes -= 1 153 | DB.Save(&cdk) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /server/sqlite/jwtSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/16 19:15 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : jwtSQL.go 6 | 7 | package sqlite 8 | 9 | import "QLPanelTools/server/model" 10 | 11 | func GetJWTKey() string { 12 | var jwt model.JWTAdmin 13 | DB.First(&jwt) 14 | return jwt.SecretKey 15 | } 16 | 17 | func CreateJWTKey(pwd string) { 18 | var jwt model.JWTAdmin 19 | jwt.SecretKey = pwd 20 | DB.Create(&jwt) 21 | } 22 | -------------------------------------------------------------------------------- /server/sqlite/messageSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/17 12:32 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : messageSQL.go 6 | 7 | package sqlite 8 | 9 | import "QLPanelTools/server/model" 10 | 11 | func InsertEmailData(email model.Email) { 12 | DB.Create(&email) 13 | } 14 | 15 | func UpdateEmailData(email *model.UpdateEmail) { 16 | var emailData model.Email 17 | DB.First(&emailData) 18 | emailData.EnableEmail = email.EnableEmail 19 | emailData.SendMail = email.SendMail 20 | emailData.SendPwd = email.SendPwd 21 | emailData.SMTPServer = email.SMTPServer 22 | emailData.SMTPPort = email.SMTPPort 23 | emailData.SendName = email.SendName 24 | DB.Save(&emailData) 25 | } 26 | 27 | func GetEmailOne() model.Email { 28 | var emailData model.Email 29 | DB.First(&emailData) 30 | return emailData 31 | } 32 | -------------------------------------------------------------------------------- /server/sqlite/openSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/7 19:21 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : openSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | ) 12 | 13 | // GetServerCount 获取可用服务器和ID值 14 | func GetServerCount() []model.QLPanel { 15 | var s []model.QLPanel 16 | DB.Where("enable = ?", true).Find(&s) 17 | return s 18 | } 19 | 20 | // CheckServerDoesItExist 检查服务器是否存在 21 | func CheckServerDoesItExist(id int) (bool, model.QLPanel) { 22 | var s model.QLPanel 23 | DB.First(&s, id) 24 | if s.ID == 0 { 25 | return false, s 26 | } 27 | 28 | return true, s 29 | } 30 | 31 | // CheckEnvNameDoesItExist 检查变量是否存在 32 | func CheckEnvNameDoesItExist(name string) (bool, model.EnvName) { 33 | var e model.EnvName 34 | DB.Where("name = ?", name).First(&e) 35 | if e.ID == 0 { 36 | return false, e 37 | } 38 | 39 | return true, e 40 | } 41 | -------------------------------------------------------------------------------- /server/sqlite/panelSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/7 16:45 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : panelSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "go.uber.org/zap" 12 | "strings" 13 | ) 14 | 15 | // InsertPanelData 创建新面板信息 16 | func InsertPanelData(data *model.PanelData) (err error) { 17 | var dData model.QLPanel 18 | 19 | if data.Name == "" { 20 | dData.PanelName = "未命名" 21 | } else { 22 | dData.PanelName = data.Name 23 | } 24 | 25 | dData.URL = data.URL 26 | dData.ClientID = data.ID 27 | dData.ClientSecret = data.Secret 28 | dData.Enable = data.Enable 29 | dData.PanelVersion = data.PanelVersion 30 | err = DB.Create(&dData).Error 31 | if err != nil { 32 | zap.L().Error("Insert data error, err:", zap.Error(err)) 33 | return 34 | } 35 | return 36 | } 37 | 38 | // UpdatePanelData 更新面板信息 39 | func UpdatePanelData(data *model.UpPanelData) { 40 | var d model.QLPanel 41 | // 通过ID查询并更新数据 42 | DB.Where("id = ? ", data.UID).First(&d) 43 | d.PanelName = data.Name 44 | d.URL = data.URL 45 | d.ClientID = data.ID 46 | d.ClientSecret = data.Secret 47 | d.Enable = data.Enable 48 | d.PanelVersion = data.PanelVersion 49 | DB.Save(&d) 50 | } 51 | 52 | // DelPanelData 删除面板信息 53 | func DelPanelData(data *model.DelPanelData) { 54 | var d model.QLPanel 55 | DB.Where("id = ? ", data.UID).First(&d) 56 | DB.Delete(&d) 57 | } 58 | 59 | // GetPanelAllData 获取面板All信息 60 | func GetPanelAllData() []model.QLPanel { 61 | var p []model.QLPanel 62 | //sqlStr := "SELECT `id`, `panel_name`, `url`, `client_id`, `client_secret`, `env_binding`, `enable` FROM `ql_panels` where `deleted_at` IS NULL;" 63 | //DB.Raw(sqlStr).Scan(&p) 64 | DB.Find(&p) 65 | return p 66 | } 67 | 68 | // UpdatePanelEnvData 更新面板绑定变量 69 | func UpdatePanelEnvData(data *model.PanelEnvData) { 70 | var d model.QLPanel 71 | // 通过ID查询并更新数据 72 | DB.Where("id = ? ", data.UID).First(&d) 73 | 74 | // []String 转换 String 储存 75 | d.EnvBinding = strings.Join(data.EnvBinding, "@") 76 | DB.Save(&d) 77 | } 78 | 79 | // GetPanelDataByID 根据ID值查询容器信息 80 | func GetPanelDataByID(id int) model.QLPanel { 81 | var d model.QLPanel 82 | // 通过ID查询容器 83 | DB.Where("id = ? ", id).First(&d) 84 | return d 85 | } 86 | 87 | // GetPanelDataByURL 根据 URL 查询面板信息 88 | func GetPanelDataByURL(url string) model.QLPanel { 89 | var d model.QLPanel 90 | // 通过URL查询面板 91 | DB.Where("url = ? ", url).First(&d) 92 | return d 93 | } 94 | 95 | // UnbindPanelEnvData 解绑面板绑定变量 96 | func UnbindPanelEnvData(p model.QLPanel) { 97 | DB.Save(&p) 98 | } 99 | -------------------------------------------------------------------------------- /server/sqlite/settingSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/21 19:58 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : settingSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | ) 12 | 13 | // GetSetting 获取一个配置信息 14 | func GetSetting(name string) (model.WebSettings, error) { 15 | var items model.WebSettings 16 | if err = DB.Where("key = ? ", name).First(&items).Error; err != nil { 17 | return items, err 18 | } 19 | return items, nil 20 | } 21 | 22 | // GetSettings 获取所有配置信息 23 | func GetSettings() ([]model.WebSettings, error) { 24 | var items []model.WebSettings 25 | if err := DB.Find(&items).Error; err != nil { 26 | return nil, err 27 | } 28 | return items, nil 29 | } 30 | 31 | // SaveSettings 保存配置信息 32 | func SaveSettings(items *[]model.WebSettings) error { 33 | return DB.Save(items).Error 34 | } 35 | -------------------------------------------------------------------------------- /server/sqlite/sqlite.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 13:08 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : sqlite.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "math/rand" 12 | "time" 13 | 14 | "go.uber.org/zap" 15 | "gorm.io/driver/sqlite" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | var DB *gorm.DB 20 | var err error 21 | 22 | func Init() *gorm.DB { 23 | // 连接MySQL 24 | DB, err = gorm.Open(sqlite.Open("config/app.db"), &gorm.Config{}) 25 | if err != nil { 26 | zap.L().Error("SQLite 发生错误") 27 | panic(err.Error()) 28 | } 29 | 30 | // 自动迁移 31 | err := DB.AutoMigrate( 32 | &model.User{}, 33 | &model.EnvName{}, 34 | &model.QLPanel{}, 35 | &model.LoginRecord{}, 36 | &model.WebSettings{}, 37 | &model.OperationRecord{}, 38 | &model.IPSubmitRecord{}, 39 | &model.Email{}, 40 | &model.CDK{}, 41 | &model.JWTAdmin{}, 42 | ) 43 | if err != nil { 44 | zap.L().Error("SQLite 自动迁移失败") 45 | panic(err.Error()) 46 | } 47 | 48 | return DB 49 | } 50 | 51 | var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 52 | 53 | func InitWebSettings() { 54 | // 判断Settings是否是第一次创建 55 | settings, err := GetSettings() 56 | if err != nil { 57 | zap.L().Error("InitWebSettings 发生错误") 58 | panic(err.Error()) 59 | } 60 | 61 | // 检查JWT密钥表是否存在 62 | jwtKey := GetJWTKey() 63 | if jwtKey == "" || len(jwtKey) < 10 { 64 | // 生成密码并写入数据库 65 | b := make([]rune, 18) 66 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 67 | for i := range b { 68 | b[i] = letters[r.Intn(62)] 69 | } 70 | zap.L().Debug("生成密钥:" + string(b)) 71 | CreateJWTKey(string(b)) 72 | } 73 | 74 | if len(settings) == 0 { 75 | zap.L().Debug("Init WebSettings") 76 | p := &[]model.WebSettings{ 77 | {Key: "notice", Value: ""}, 78 | {Key: "blacklist", Value: ""}, 79 | {Key: "backgroundImage", Value: ""}, 80 | {Key: "ipCount", Value: "0"}, 81 | {Key: "ghProxy", Value: "https://ghproxy.com"}, 82 | {Key: "webTitle", Value: "青龙Tools"}, 83 | } 84 | 85 | err = SaveSettings(p) 86 | if err != nil { 87 | zap.L().Error("InitWebSettings 发生错误") 88 | panic(err.Error()) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /server/sqlite/tokenSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/7 16:30 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : tokenSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | // SaveToken 储存Token 15 | func SaveToken(url, token string, params int) { 16 | var t model.QLPanel 17 | zap.L().Debug("SaveTokenUrl:" + url) 18 | // 通过URL查询并更新数据 19 | DB.Where("url = ?", url).First(&t) 20 | t.Token = token 21 | t.Params = params 22 | DB.Save(&t) 23 | } 24 | -------------------------------------------------------------------------------- /server/sqlite/userSQL.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:21 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : userSQL.go 6 | 7 | package sqlite 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | res "QLPanelTools/tools/response" 12 | "QLPanelTools/tools/timeTools" 13 | "time" 14 | ) 15 | 16 | // InsertUser 创建新用户 17 | func InsertUser(user *model.User) (err error) { 18 | err = DB.Create(&user).Error 19 | if err != nil { 20 | return 21 | } 22 | return 23 | } 24 | 25 | // GetUserData 获取用户信息 26 | func GetUserData() (bool, model.User) { 27 | var user model.User 28 | // 获取键值第一条记录 29 | DB.First(&user) 30 | 31 | // 判断是否已注册 32 | if user.Username != "" { 33 | return true, user 34 | } else { 35 | return false, user 36 | } 37 | } 38 | 39 | // CheckEmail 检查邮箱是否存在 40 | func CheckEmail(email string) res.ResCode { 41 | var user model.User 42 | DB.Where("email = ?", email).First(&user) 43 | if user.ID != 0 { 44 | // 邮箱已存在 45 | return res.CodeEmailExist 46 | } else { 47 | // 邮箱不存在 48 | return res.CodeEmailNotExist 49 | } 50 | } 51 | 52 | // CheckAdmin 判断是否属于管理员 53 | func CheckAdmin(userID interface{}) bool { 54 | var user model.User 55 | DB.Where("user_id = ?", userID).First(&user) 56 | if user.Username != "" { 57 | return true 58 | } else { 59 | return false 60 | } 61 | } 62 | 63 | // UpdateUserData 更新用户信息 64 | func UpdateUserData(email, pwd string) error { 65 | var user model.User 66 | // 获取管理员信息 67 | result := DB.First(&user) 68 | if result.Error != nil { 69 | return result.Error 70 | } 71 | // 更新数据 72 | if email != "" { 73 | user.Email = email 74 | } 75 | user.Password = pwd 76 | // 储存数据 77 | result = DB.Save(&user) 78 | if result.Error != nil { 79 | return result.Error 80 | } 81 | return nil 82 | } 83 | 84 | // InsertLoginRecord 创建登录记录 85 | func InsertLoginRecord(lr *model.LoginRecord) { 86 | DB.Create(&lr) 87 | } 88 | 89 | // GetIPData 获取IP记录 90 | func GetIPData() []model.IpData { 91 | var i []model.IpData 92 | sqlStr := "SELECT `login_time`, `ip`, `ip_address`, `if_ok` FROM `login_records` order by id desc limit 0,10;" 93 | DB.Raw(sqlStr).Scan(&i) 94 | return i 95 | } 96 | 97 | // GetFailLoginIPData 获取当日登录IP记录 98 | func GetFailLoginIPData() []model.IpData { 99 | var i []model.IpData 100 | sqlStr := "SELECT `login_day`, `login_time`, `ip`, `ip_address`, `if_ok` FROM `login_records` WHERE login_day = '" + timeTools.SwitchTimeStampToDataYear(time.Now().Unix()) + "';" 101 | DB.Raw(sqlStr).Scan(&i) 102 | return i 103 | } 104 | -------------------------------------------------------------------------------- /static/assets/css/134.2b81fd4b.css: -------------------------------------------------------------------------------- 1 | .box[data-v-040c69f2]{margin:0 auto;width:61.8%;padding:30px;background:hsla(0,9%,98%,.8);box-sizing:border-box;box-shadow:0 15px 25px rgba(60,64,67,.25);border-radius:15px}@media screen and (max-width:912px){.box[data-v-040c69f2]{width:80%}}@media screen and (max-width:540px){.box[data-v-040c69f2]{width:500px}}@media screen and (max-width:420px){.box[data-v-040c69f2]{width:360px}}@media screen and (max-width:375px){.box[data-v-040c69f2]{width:330px}}@media screen and (max-width:280px){.box[data-v-040c69f2]{width:255px}}.notice_data[data-v-040c69f2]{line-height:30px;padding-top:10px}.envInput[data-v-040c69f2]{width:70%}#envSelect[data-v-040c69f2],.envInput[data-v-040c69f2]{text-align:center;margin:20px auto 0}footer[data-v-326b083d]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}@media screen and (min-height:800px){footer[data-v-326b083d]{bottom:0;width:100%}}st[data-v-326b083d]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/177.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/246.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/255.c768f918.css: -------------------------------------------------------------------------------- 1 | footer[data-v-326b083d]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}@media screen and (min-height:800px){footer[data-v-326b083d]{bottom:0;width:100%}}st[data-v-326b083d]{font-weight:600;color:#475bca}.box[data-v-21bbbd6c]{margin:0 auto;width:61.8%;padding:30px;background:hsla(0,9%,98%,.8);box-sizing:border-box;box-shadow:0 15px 25px rgba(60,64,67,.25);border-radius:15px}@media screen and (max-width:912px){.box[data-v-21bbbd6c]{width:80%}}@media screen and (max-width:540px){.box[data-v-21bbbd6c]{width:500px}}@media screen and (max-width:420px){.box[data-v-21bbbd6c]{width:360px}}@media screen and (max-width:375px){.box[data-v-21bbbd6c]{width:330px}}@media screen and (max-width:280px){.box[data-v-21bbbd6c]{width:255px}} -------------------------------------------------------------------------------- /static/assets/css/362.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/452.ed4ca03e.css: -------------------------------------------------------------------------------- 1 | footer[data-v-326b083d]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}@media screen and (min-height:800px){footer[data-v-326b083d]{bottom:0;width:100%}}st[data-v-326b083d]{font-weight:600;color:#475bca}.box[data-v-7d085c26]{margin:0 auto;width:61.8%;padding:30px;background:hsla(0,9%,98%,.8);box-sizing:border-box;box-shadow:0 15px 25px rgba(60,64,67,.25);border-radius:15px}@media screen and (max-width:912px){.box[data-v-7d085c26]{width:80%}}@media screen and (max-width:540px){.box[data-v-7d085c26]{width:500px}}@media screen and (max-width:420px){.box[data-v-7d085c26]{width:360px}}@media screen and (max-width:375px){.box[data-v-7d085c26]{width:330px}}@media screen and (max-width:280px){.box[data-v-7d085c26]{width:255px}} -------------------------------------------------------------------------------- /static/assets/css/534.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/557.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/647.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/657.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/923.8f8f9b74.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca} -------------------------------------------------------------------------------- /static/assets/css/946.e1dedb28.css: -------------------------------------------------------------------------------- 1 | .mdui-drawer[data-v-11e0cc88],footer[data-v-03fba56c]{-webkit-backdrop-filter:blur(15px) brightness(110%);backdrop-filter:blur(15px) brightness(110%)}footer[data-v-03fba56c]{width:100%;text-align:center;box-shadow:5px 0 5px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}@media screen and (min-height:800px){footer[data-v-03fba56c]{bottom:0;width:100%}}st[data-v-03fba56c]{font-weight:600;color:#475bca}:host,:root{--w-e-textarea-bg-color:#fff;--w-e-textarea-color:#333;--w-e-textarea-border-color:#ccc;--w-e-textarea-slight-border-color:#e8e8e8;--w-e-textarea-slight-color:#d4d4d4;--w-e-textarea-slight-bg-color:#f5f2f0;--w-e-textarea-selected-border-color:#b4d5ff;--w-e-textarea-handler-bg-color:#4290f7;--w-e-toolbar-color:#595959;--w-e-toolbar-bg-color:#fff;--w-e-toolbar-active-color:#333;--w-e-toolbar-active-bg-color:#f1f1f1;--w-e-toolbar-disabled-color:#999;--w-e-toolbar-border-color:#e8e8e8;--w-e-modal-button-bg-color:#fafafa;--w-e-modal-button-border-color:#d9d9d9}.w-e-text-container *,.w-e-toolbar *{box-sizing:border-box;margin:0;outline:none;padding:0}.w-e-text-container blockquote,.w-e-text-container li,.w-e-text-container p,.w-e-text-container td,.w-e-text-container th,.w-e-toolbar *{line-height:1.5}.w-e-text-container{background-color:var(--w-e-textarea-bg-color);color:var(--w-e-textarea-color);height:100%;position:relative}.w-e-text-container .w-e-scroll{-webkit-overflow-scrolling:touch;height:100%}.w-e-text-container [data-slate-editor]{word-wrap:break-word;border-top:1px solid transparent;min-height:100%;outline:0;padding:0 10px;white-space:pre-wrap}.w-e-text-container [data-slate-editor] p{margin:15px 0}.w-e-text-container [data-slate-editor] h1,.w-e-text-container [data-slate-editor] h2,.w-e-text-container [data-slate-editor] h3,.w-e-text-container [data-slate-editor] h4,.w-e-text-container [data-slate-editor] h5{margin:20px 0}.w-e-text-container [data-slate-editor] img{cursor:default;display:inline!important;max-width:100%;min-height:20px;min-width:20px}.w-e-text-container [data-slate-editor] [data-selected=true]{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-placeholder{font-style:italic;left:10px;top:17px;width:90%}.w-e-max-length-info,.w-e-text-placeholder{color:var(--w-e-textarea-slight-color);pointer-events:none;position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none}.w-e-max-length-info{bottom:.5em;right:1em}.w-e-bar{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-color);font-size:14px;padding:0 5px}.w-e-bar svg{fill:var(--w-e-toolbar-color);height:14px;width:14px}.w-e-bar-show{display:flex}.w-e-bar-hidden{display:none}.w-e-hover-bar{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 5px #0000001f;position:absolute}.w-e-toolbar{flex-wrap:wrap;position:relative}.w-e-bar-divider{background-color:var(--w-e-toolbar-border-color);display:inline-flex;height:40px;margin:0 5px;width:1px}.w-e-bar-item{display:flex;height:40px;padding:4px;position:relative;text-align:center}.w-e-bar-item,.w-e-bar-item button{align-items:center;justify-content:center}.w-e-bar-item button{background:transparent;border:none;color:var(--w-e-toolbar-color);cursor:pointer;display:inline-flex;height:32px;overflow:hidden;padding:0 8px;white-space:nowrap}.w-e-bar-item button:hover{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item button .title{margin-left:5px}.w-e-bar-item .active{background-color:var(--w-e-toolbar-active-bg-color);color:var(--w-e-toolbar-active-color)}.w-e-bar-item .disabled{color:var(--w-e-toolbar-disabled-color);cursor:not-allowed}.w-e-bar-item .disabled svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover{background-color:var(--w-e-toolbar-bg-color);color:var(--w-e-toolbar-disabled-color)}.w-e-bar-item .disabled:hover svg{fill:var(--w-e-toolbar-disabled-color)}.w-e-menu-tooltip-v5:before{background-color:var(--w-e-toolbar-active-color);border-radius:5px;color:var(--w-e-toolbar-bg-color);content:attr(data-tooltip);font-size:.75em;padding:5px 10px;text-align:center;top:40px;white-space:pre;z-index:1}.w-e-menu-tooltip-v5:after,.w-e-menu-tooltip-v5:before{opacity:0;position:absolute;transition:opacity .6s;visibility:hidden}.w-e-menu-tooltip-v5:after{border:5px solid transparent;border-bottom:5px solid var(--w-e-toolbar-active-color);content:"";top:30px}.w-e-menu-tooltip-v5:hover:after,.w-e-menu-tooltip-v5:hover:before{opacity:1;visibility:visible}.w-e-menu-tooltip-v5.tooltip-right:before{left:100%;top:10px}.w-e-menu-tooltip-v5.tooltip-right:after{border-bottom-color:transparent;border-left-color:transparent;border-right-color:var(--w-e-toolbar-active-color);border-top-color:transparent;left:100%;margin-left:-10px;top:16px}.w-e-bar-item-group .w-e-bar-item-menus-container{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;display:none;left:0;margin-top:40px;position:absolute;top:0;z-index:1}.w-e-bar-item-group:hover .w-e-bar-item-menus-container{display:block}.w-e-select-list{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;left:0;margin-top:40px;max-height:350px;min-width:100px;overflow-y:auto;position:absolute;top:0;z-index:1}.w-e-select-list ul{line-height:1;list-style:none}.w-e-select-list ul .selected{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li{cursor:pointer;padding:7px 0 7px 25px;position:relative;text-align:left;white-space:nowrap}.w-e-select-list ul li:hover{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-select-list ul li svg{left:0;margin-left:5px;margin-top:-7px;position:absolute;top:50%}.w-e-bar-bottom .w-e-select-list{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-drop-panel{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;margin-top:40px;min-width:200px;padding:10px;position:absolute;top:0;z-index:1}.w-e-bar-bottom .w-e-drop-panel{bottom:0;margin-bottom:40px;margin-top:0;top:inherit}.w-e-modal{background-color:var(--w-e-toolbar-bg-color);border:1px solid var(--w-e-toolbar-border-color);border-radius:3px;box-shadow:0 2px 10px #0000001f;color:var(--w-e-toolbar-color);font-size:14px;min-height:40px;min-width:100px;padding:20px 15px 0;position:absolute;text-align:left;z-index:1}.w-e-modal .btn-close{cursor:pointer;line-height:1;padding:5px;position:absolute;right:8px;top:7px}.w-e-modal .btn-close svg{fill:var(--w-e-toolbar-color);height:10px;width:10px}.w-e-modal .babel-container{display:block;margin-bottom:15px}.w-e-modal .babel-container span{display:block;margin-bottom:10px}.w-e-modal .button-container{margin-bottom:15px}.w-e-modal button{background-color:var(--w-e-modal-button-bg-color);cursor:pointer;font-weight:400;height:32px;padding:4.5px 15px;text-align:center;touch-action:manipulation;transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.w-e-modal button,.w-e-modal input[type=number],.w-e-modal input[type=text],.w-e-modal textarea{border:1px solid var(--w-e-modal-button-border-color);border-radius:4px;color:var(--w-e-toolbar-color)}.w-e-modal input[type=number],.w-e-modal input[type=text],.w-e-modal textarea{font-feature-settings:"tnum";background-color:var(--w-e-toolbar-bg-color);font-variant:tabular-nums;padding:4.5px 11px;transition:all .3s;width:100%}.w-e-modal textarea{min-height:60px}body .w-e-modal,body .w-e-modal *{box-sizing:border-box}.w-e-progress-bar{background-color:var(--w-e-textarea-handler-bg-color);height:1px;position:absolute;transition:width .3s;width:0}.w-e-full-screen-container{bottom:0!important;display:flex!important;flex-direction:column!important;height:100%!important;left:0!important;margin:0!important;padding:0!important;position:fixed;right:0!important;top:0!important;width:100%!important}.w-e-full-screen-container [data-w-e-textarea=true]{flex:1!important}.w-e-text-container [data-slate-editor] code{background-color:var(--w-e-textarea-slight-bg-color);border-radius:3px;font-family:monospace;padding:3px}.w-e-panel-content-color{list-style:none;text-align:left;width:230px}.w-e-panel-content-color li{border:1px solid var(--w-e-toolbar-bg-color);border-radius:3px 3px;cursor:pointer;display:inline-block;padding:2px}.w-e-panel-content-color li:hover{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color li .color-block{border:1px solid var(--w-e-toolbar-border-color);border-radius:3px 3px;height:17px;width:17px}.w-e-panel-content-color .active{border-color:var(--w-e-toolbar-color)}.w-e-panel-content-color .clear{line-height:1.5;margin-bottom:5px;width:100%}.w-e-panel-content-color .clear svg{height:16px;margin-bottom:-4px;width:16px}.w-e-text-container [data-slate-editor] blockquote{background-color:var(--w-e-textarea-slight-bg-color);border-left:8px solid var(--w-e-textarea-selected-border-color);display:block;font-size:100%;line-height:1.5;margin:10px 0;padding:10px}.w-e-panel-content-emotion{font-size:20px;list-style:none;text-align:left;width:300px}.w-e-panel-content-emotion li{border-radius:3px 3px;cursor:pointer;display:inline-block;padding:0 5px}.w-e-panel-content-emotion li:hover{background-color:var(--w-e-textarea-slight-bg-color)}.w-e-textarea-divider{border-radius:3px;margin:20px auto;padding:20px}.w-e-textarea-divider hr{background-color:var(--w-e-textarea-border-color);border:0;display:block;height:1px}.w-e-text-container [data-slate-editor] pre>code{background-color:var(--w-e-textarea-slight-bg-color);border:1px solid var(--w-e-textarea-slight-border-color);border-radius:4px 4px;display:block;font-size:14px;padding:10px;text-indent:0}.w-e-text-container [data-slate-editor] .w-e-image-container{display:inline-block;margin:0 10px}.w-e-text-container [data-slate-editor] .w-e-image-container:hover{box-shadow:0 0 0 2px var(--w-e-textarea-selected-border-color)}.w-e-text-container [data-slate-editor] .w-e-selected-image-container{overflow:hidden;position:relative}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .w-e-image-dragger{background-color:var(--w-e-textarea-handler-bg-color);height:7px;position:absolute;width:7px}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-top{cursor:nwse-resize;left:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-top{cursor:nesw-resize;right:0;top:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .left-bottom{bottom:0;cursor:nesw-resize;left:0}.w-e-text-container [data-slate-editor] .w-e-selected-image-container .right-bottom{bottom:0;cursor:nwse-resize;right:0}.w-e-text-container [contenteditable=false] .w-e-image-container:hover,.w-e-text-container [data-slate-editor] .w-e-selected-image-container:hover{box-shadow:none}.w-e-text-container [data-slate-editor] ol,.w-e-text-container [data-slate-editor] ul{padding-left:20px}.w-e-text-container [data-slate-editor] li{line-height:inherit;margin:10px 0}.w-e-text-container [data-slate-editor] table{border-collapse:collapse}.w-e-text-container [data-slate-editor] table td,.w-e-text-container [data-slate-editor] table th{border:1px solid var(--w-e-textarea-border-color);line-height:1.5;min-width:50px;padding:3px 5px;text-align:left}.w-e-text-container [data-slate-editor] table th{background-color:var(--w-e-textarea-slight-bg-color);font-weight:700;text-align:center}.w-e-text-container [data-slate-editor] table.full-width{width:100%}.w-e-text-container [data-slate-editor] table.full-width td.th{min-width:0}.w-e-panel-content-table{background-color:var(--w-e-toolbar-bg-color)}.w-e-panel-content-table table{border-collapse:collapse}.w-e-panel-content-table td{border:1px solid var(--w-e-toolbar-border-color);cursor:pointer;height:15px;padding:3px 5px;width:20px}.w-e-panel-content-table td.active{background-color:var(--w-e-toolbar-active-bg-color)}.w-e-textarea-video-container{border:1px solid var(--w-e-textarea-border-color);margin:0 auto;padding:10px 0;text-align:center;width:480px}.w-e-textarea-video-container iframe{height:245px;width:450px}.w-e-textarea-video-container video{width:450px}.w-e-text-container [data-slate-editor] pre>code{word-wrap:normal;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;-webkit-hyphens:none;hyphens:none;line-height:1.5;margin:.5em 0;overflow:auto;padding:1em;-moz-tab-size:4;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 1px #fff;white-space:pre;word-break:normal;word-spacing:normal}.w-e-text-container [data-slate-editor] pre>code .token.cdata,.w-e-text-container [data-slate-editor] pre>code .token.comment,.w-e-text-container [data-slate-editor] pre>code .token.doctype,.w-e-text-container [data-slate-editor] pre>code .token.prolog{color:#708090}.w-e-text-container [data-slate-editor] pre>code .token.punctuation{color:#999}.w-e-text-container [data-slate-editor] pre>code .token.namespace{opacity:.7}.w-e-text-container [data-slate-editor] pre>code .token.boolean,.w-e-text-container [data-slate-editor] pre>code .token.constant,.w-e-text-container [data-slate-editor] pre>code .token.deleted,.w-e-text-container [data-slate-editor] pre>code .token.number,.w-e-text-container [data-slate-editor] pre>code .token.property,.w-e-text-container [data-slate-editor] pre>code .token.symbol,.w-e-text-container [data-slate-editor] pre>code .token.tag{color:#905}.w-e-text-container [data-slate-editor] pre>code .token.attr-name,.w-e-text-container [data-slate-editor] pre>code .token.builtin,.w-e-text-container [data-slate-editor] pre>code .token.char,.w-e-text-container [data-slate-editor] pre>code .token.inserted,.w-e-text-container [data-slate-editor] pre>code .token.selector,.w-e-text-container [data-slate-editor] pre>code .token.string{color:#690}.w-e-text-container [data-slate-editor] pre>code .language-css .token.string,.w-e-text-container [data-slate-editor] pre>code .style .token.string,.w-e-text-container [data-slate-editor] pre>code .token.entity,.w-e-text-container [data-slate-editor] pre>code .token.operator,.w-e-text-container [data-slate-editor] pre>code .token.url{color:#9a6e3a}.w-e-text-container [data-slate-editor] pre>code .token.atrule,.w-e-text-container [data-slate-editor] pre>code .token.attr-value,.w-e-text-container [data-slate-editor] pre>code .token.keyword{color:#07a}.w-e-text-container [data-slate-editor] pre>code .token.class-name,.w-e-text-container [data-slate-editor] pre>code .token.function{color:#dd4a68}.w-e-text-container [data-slate-editor] pre>code .token.important,.w-e-text-container [data-slate-editor] pre>code .token.regex,.w-e-text-container [data-slate-editor] pre>code .token.variable{color:#e90}.w-e-text-container [data-slate-editor] pre>code .token.bold,.w-e-text-container [data-slate-editor] pre>code .token.important{font-weight:700}.w-e-text-container [data-slate-editor] pre>code .token.italic{font-style:italic}.w-e-text-container [data-slate-editor] pre>code .token.entity{cursor:help}#background-image[data-v-330ca1a4],#notice[data-v-330ca1a4],#submission-restrictions[data-v-330ca1a4],#textfield[data-v-330ca1a4]{margin-right:32px;margin-left:32px}.mdui-card-content[data-v-330ca1a4]{line-height:10px} -------------------------------------------------------------------------------- /static/assets/css/app.568720ed.css: -------------------------------------------------------------------------------- 1 | *{margin:0;padding:0;box-sizing:border-box}body{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1600 900'%3E%3Cpath fill='%23f6c5d3' d='M957 450 539 900h857z'/%3E%3Cpath fill='%237b8aca' d='m957 450-84.1 450H1396z'/%3E%3Cpath fill='%23ecc7e1' d='m-60 900 458-238 418 238z'/%3E%3Cpath fill='%237491d0' d='m337 900 61-238 418 238z'/%3E%3Cpath fill='%23d9c6eb' d='m1203 546 349 354H876z'/%3E%3Cpath fill='%236792cc' d='m1203 546 349 354h-390z'/%3E%3Cpath fill='%23c4caf3' d='m641 695 245 205H367z'/%3E%3Cpath fill='%236caae6' d='m587 900 54-205 245 205z'/%3E%3Cpath fill='%23afd3f5' d='m1710 900-309-268-305 268z'/%3E%3Cpath fill='%2374bef4' d='m1710 900-309-268-36 268z'/%3E%3Cpath fill='%239ddcfa' d='M1210 900 971 687 725 900z'/%3E%3Cpath fill='%2369c3f8' d='M943 900h267L971 687z'/%3E%3C/svg%3E");background-attachment:fixed;background-size:cover;background-color:hsla(0,0%,100%,.6);--color-border:none}pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline} -------------------------------------------------------------------------------- /static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuanxinqing123/QLTools/dbb09200d5dc23e1f5e896af0723e26b1535cd90/static/assets/favicon.ico -------------------------------------------------------------------------------- /static/assets/index.html: -------------------------------------------------------------------------------- 1 | 青龙Tools
-------------------------------------------------------------------------------- /static/assets/js/452.9478c9cf.js: -------------------------------------------------------------------------------- 1 | var a2_0x3c858b=a2_0x2a50;(function(_0x2db8fb,_0xeef456){var _0x4480bf=a2_0x2a50,_0x24ff3f=_0x2db8fb();while(!![]){try{var _0x27a533=parseInt(_0x4480bf(0x250))/0x1*(-parseInt(_0x4480bf(0x1fe))/0x2)+parseInt(_0x4480bf(0x24e))/0x3+-parseInt(_0x4480bf(0x24b))/0x4+-parseInt(_0x4480bf(0x217))/0x5*(-parseInt(_0x4480bf(0x254))/0x6)+parseInt(_0x4480bf(0x257))/0x7+parseInt(_0x4480bf(0x1db))/0x8*(-parseInt(_0x4480bf(0x1ef))/0x9)+parseInt(_0x4480bf(0x252))/0xa*(parseInt(_0x4480bf(0x20b))/0xb);if(_0x27a533===_0xeef456)break;else _0x24ff3f['push'](_0x24ff3f['shift']());}catch(_0x5a3ddb){_0x24ff3f['push'](_0x24ff3f['shift']());}}}(a2_0x1999,0xef905));function a2_0x2a50(_0x3d23d9,_0x1d4ff7){var _0x2a2ef4=a2_0x1999();return a2_0x2a50=function(_0x53b430,_0x5bb5ff){_0x53b430=_0x53b430-0x1d9;var _0x53c088=_0x2a2ef4[_0x53b430];return _0x53c088;},a2_0x2a50(_0x3d23d9,_0x1d4ff7);}function a2_0x1999(){var _0x57f2e0=['prototype','function\x20*\x5c(\x20*\x5c)','constructor','style','bind','password','__proto__','mdui-p-a-5\x20mdui-typo\x20mdui-container','render','__scopeId','input','_self','4989512InEmgR','mdui-ripple\x20mdui-btn\x20mdui-btn-raised\x20mdui-ripple\x20mdui-color-blue-50','mdui-icon\x20material-icons','1800546AMnloV','init','5BmlXCB','Dialog','730WMppnw','HeaderCom','12TYwKmw','footer','data-v-7d085c26','4258233BwxxpV','toString','email','right-top','80etRnUL','{content:\x20\x27退出登录\x27}','确认密钥','20px\x20auto\x200','{content:\x20\x27密钥添加/更新\x27}','{}.constructor(\x22return\x20this\x22)(\x20)','querySelector','inline','body','info','管理员登录','35px','while\x20(true)\x20{}','prevent','mdui-toolbar-spacer','LoginView','form','/#/admin','apply','OpenCDKInput','612558MZCXlP','#box','account_circle','post','FooterCom','LogOut','{content:\x20\x27管理面板\x27}','GoAdmin','box','clientHeight','clear','stateObject','then','mdui-btn\x20mdui-btn-icon\x20mdui-ripple','webTitle','240954ElWBEp','20px','服务繁忙','青龙Tools','account_balance_wallet','mdui-textfield\x20mdui-textfield-floating-label','青龙\x20Tools','/#/','getItem','log','退出登录','open','mdui-btn\x20mdui-btn-icon','287771aHeMyd','登\u2002\u2002录','data-v-326b083d','LoginData','return\x20(function()\x20','mdui-toolbar','#cdkAdd','CDK_Btn','\x5c+\x5c+\x20*(?:[a-zA-Z_$][0-9a-zA-Z_$]*)','display','toggle','trace','983455CZdfGS','200px','/v1/api/set/setting?key=webTitle','table','center','Admin','msg','value','get','length','none','code','chain','webpackChunkqltools','GetWebTitle','counter','mdui-textfield-input','Bearer','mutation','action','innerHeight','data','mdui-typo-title','CD_KEY','75%','debu','submitForm','reload','refresh','邮箱格式错误','credit_card','cdk','https://github.com/nuanxinqing123/QLTools','button','Copyright','getElementById','changeStyle','CheckLogin','div','console'];a2_0x1999=function(){return _0x57f2e0;};return a2_0x1999();}var a2_0x2378d0=(function(){var _0xf89ed0=!![];return function(_0x165579,_0x52e38d){var _0x56784f=_0xf89ed0?function(){var _0x5563ad=a2_0x2a50;if(_0x52e38d){var _0x75c8ff=_0x52e38d[_0x5563ad(0x1ed)](_0x165579,arguments);return _0x52e38d=null,_0x75c8ff;}}:function(){};return _0xf89ed0=![],_0x56784f;};}());(function(){a2_0x2378d0(this,function(){var _0x365987=a2_0x2a50,_0x21ccd4=new RegExp(_0x365987(0x240)),_0x27c64a=new RegExp(_0x365987(0x213),'i'),_0x211b6f=a2_0x119454(_0x365987(0x24f));!_0x21ccd4['test'](_0x211b6f+_0x365987(0x223))||!_0x27c64a['test'](_0x211b6f+_0x365987(0x249))?_0x211b6f('0'):a2_0x119454();})();}());var a2_0x5bb5ff=(function(){var _0x4951b5=!![];return function(_0x44325e,_0xb7e87){var _0x31bcd9=_0x4951b5?function(){if(_0xb7e87){var _0x130e88=_0xb7e87['apply'](_0x44325e,arguments);return _0xb7e87=null,_0x130e88;}}:function(){};return _0x4951b5=![],_0x31bcd9;};}()),a2_0x53b430=a2_0x5bb5ff(this,function(){var _0x2afda3=a2_0x2a50,_0x2856fa=function(){var _0x450ad5=a2_0x2a50,_0x45aaaf;try{_0x45aaaf=Function(_0x450ad5(0x20f)+_0x450ad5(0x1e0)+');')();}catch(_0x4f44c8){_0x45aaaf=window;}return _0x45aaaf;},_0x5f4eed=_0x2856fa(),_0x4c05f4=_0x5f4eed['console']=_0x5f4eed[_0x2afda3(0x23e)]||{},_0x202477=[_0x2afda3(0x207),'warn',_0x2afda3(0x1e4),'error','exception',_0x2afda3(0x21a),_0x2afda3(0x216)];for(var _0x5eac24=0x0;_0x5eac24<_0x202477[_0x2afda3(0x220)];_0x5eac24++){var _0x5688fa=a2_0x5bb5ff[_0x2afda3(0x241)][_0x2afda3(0x23f)][_0x2afda3(0x243)](a2_0x5bb5ff),_0x18976d=_0x202477[_0x5eac24],_0x4ec339=_0x4c05f4[_0x18976d]||_0x5688fa;_0x5688fa[_0x2afda3(0x245)]=a2_0x5bb5ff[_0x2afda3(0x243)](a2_0x5bb5ff),_0x5688fa[_0x2afda3(0x258)]=_0x4ec339[_0x2afda3(0x258)]['bind'](_0x4ec339),_0x4c05f4[_0x18976d]=_0x5688fa;}});a2_0x53b430();'use strict';(self[a2_0x3c858b(0x224)]=self[a2_0x3c858b(0x224)]||[])['push']([[0x1c4],{0xed6:function(_0x4d053e,_0x55c470,_0x175ef2){var _0x40ae44=a2_0x3c858b;_0x175ef2['d'](_0x55c470,{'Z':function(){return _0x1e7e32;}});var _0x39537e=_0x175ef2(0x186c);const _0x4c210b=_0x524d3b=>((0x0,_0x39537e['dD'])(_0x40ae44(0x20d)),_0x524d3b=_0x524d3b(),(0x0,_0x39537e['Cn'])(),_0x524d3b),_0x2584aa={'class':'mdui-m-t-5','id':_0x40ae44(0x255)},_0x213f2d={'class':_0x40ae44(0x246)},_0x27266c={'class':'mdui-col\x20mdui-text'},_0x5b45f6=_0x4c210b(()=>(0x0,_0x39537e['_'])('span',{'id':_0x40ae44(0x239)},'Copyright\x20©\u00a02022\u00a0All\x20rights\x20reserved.\x20',-0x1)),_0x4fdb8d={'href':_0x40ae44(0x237)},_0x1c6c5f=(0x0,_0x39537e['Uk'])(_0x40ae44(0x204)),_0x5df80b=_0x4c210b(()=>(0x0,_0x39537e['_'])('p',null,'简单便捷的第三方工具,给您带来不一样的体验!',-0x1));function _0x5edf68(_0x53856c,_0x233a0a,_0xdd661c,_0x396807,_0x12a557,_0x4a78b8){var _0x5f2506=_0x40ae44;const _0x38d6b6=(0x0,_0x39537e['up'])('st');return(0x0,_0x39537e['wg'])(),(0x0,_0x39537e['iD'])(_0x5f2506(0x255),_0x2584aa,[(0x0,_0x39537e['_'])('div',_0x213f2d,[(0x0,_0x39537e['_'])(_0x5f2506(0x23d),_0x27266c,[(0x0,_0x39537e['_'])('p',null,[_0x5b45f6,(0x0,_0x39537e['_'])('a',_0x4fdb8d,[(0x0,_0x39537e['Wm'])(_0x38d6b6,null,{'default':(0x0,_0x39537e['w5'])(()=>[_0x1c6c5f]),'_':0x1})])]),_0x5df80b])])]);}var _0x25bc6c={'name':_0x40ae44(0x1f3)},_0x2c9a86=_0x175ef2(0xea0);const _0x1ba43e=(0x0,_0x2c9a86['Z'])(_0x25bc6c,[[_0x40ae44(0x247),_0x5edf68],['__scopeId',_0x40ae44(0x20d)]]);var _0x1e7e32=_0x1ba43e;},0xeea:function(_0x363f74,_0xebdca3,_0xc942c3){var _0x633408=a2_0x3c858b;_0xc942c3['d'](_0xebdca3,{'Z':function(){return _0x276270;}});var _0x3b6c6c=_0xc942c3(0x186c),_0x11844a=_0xc942c3(0xdf9);const _0x50c6c3={'class':_0x633408(0x210)},_0x112c1d=(0x0,_0x3b6c6c['_'])('i',{'class':_0x633408(0x24d)},_0x633408(0x202),-0x1),_0x14ad14={'class':_0x633408(0x22d)},_0x56461e=(0x0,_0x3b6c6c['_'])(_0x633408(0x23d),{'class':_0x633408(0x1e9)},null,-0x1),_0x99c7c1=(0x0,_0x3b6c6c['_'])('i',{'class':'mdui-icon\x20material-icons'},_0x633408(0x235),-0x1),_0x252620=[_0x99c7c1],_0x56811b=(0x0,_0x3b6c6c['_'])('a',{'mdui-tooltip':'{content:\x20\x27刷新页面\x27}','href':'javascript:location.reload();','class':_0x633408(0x1fc)},[(0x0,_0x3b6c6c['_'])('i',{'class':_0x633408(0x24d)},_0x633408(0x233))],-0x1),_0xed324b=(0x0,_0x3b6c6c['_'])('i',{'class':_0x633408(0x24d)},_0x633408(0x1f1),-0x1),_0x3e826d=[_0xed324b],_0x243a4f=(0x0,_0x3b6c6c['_'])('i',{'class':_0x633408(0x24d)},'exit_to_app',-0x1),_0x54378e=[_0x243a4f];function _0x1a17a4(_0x401ab5,_0x3eddb7,_0x1ab42c,_0x20c482,_0x5e7267,_0x52217d){var _0xcb3df5=_0x633408;return(0x0,_0x3b6c6c['wg'])(),(0x0,_0x3b6c6c['iD'])('div',_0x50c6c3,[_0x112c1d,(0x0,_0x3b6c6c['_'])('span',_0x14ad14,(0x0,_0x11844a['zw'])(_0x5e7267[_0xcb3df5(0x1fd)]),0x1),_0x56461e,(0x0,_0x3b6c6c['_'])('a',{'mdui-tooltip':_0xcb3df5(0x1df),'onClick':_0x3eddb7[0x0]||(_0x3eddb7[0x0]=_0x10a5a9=>_0x52217d[_0xcb3df5(0x1ee)]()),'class':_0xcb3df5(0x20a)},_0x252620),_0x56811b,(0x0,_0x3b6c6c['_'])('a',{'mdui-tooltip':_0xcb3df5(0x1f5),'onClick':_0x3eddb7[0x1]||(_0x3eddb7[0x1]=_0x5528c2=>this[_0xcb3df5(0x1f6)]()),'id':_0xcb3df5(0x21c),'class':_0xcb3df5(0x1fc),'style':{'display':_0xcb3df5(0x221)}},_0x3e826d),(0x0,_0x3b6c6c['_'])('a',{'mdui-tooltip':_0xcb3df5(0x1dc),'onClick':_0x3eddb7[0x2]||(_0x3eddb7[0x2]=_0x46c421=>this[_0xcb3df5(0x1f4)]()),'id':_0xcb3df5(0x1f4),'class':_0xcb3df5(0x1fc),'style':{'display':'none'}},_0x54378e)]);}var _0xd6c03a=_0xc942c3(0x25c5),_0x5b29fa=_0xc942c3['n'](_0xd6c03a),_0x4ed443=_0xc942c3(0x118d),_0x10e26d={'name':_0x633408(0x253),'data'(){var _0x33b27c=_0x633408;return{'webTitle':'','CDK_Btn':_0x33b27c(0x1dd),'CD_KEY':''};},'methods':{'GetWebTitle'(){var _0x2b6f11=_0x633408;_0x5b29fa()[_0x2b6f11(0x21f)](_0x2b6f11(0x219))['then'](_0x4ebd4d=>{var _0x5eade8=_0x2b6f11;''===_0x4ebd4d[_0x5eade8(0x22c)]['data'][_0x5eade8(0x21e)]?this['webTitle']=_0x5eade8(0x201):this[_0x5eade8(0x1fd)]=_0x4ebd4d[_0x5eade8(0x22c)][_0x5eade8(0x22c)][_0x5eade8(0x21e)];});},'CheckLogin'(){var _0x2c8fd9=_0x633408;this[_0x2c8fd9(0x22e)]=localStorage[_0x2c8fd9(0x206)](_0x2c8fd9(0x236));let _0x3ecff4=localStorage[_0x2c8fd9(0x206)](_0x2c8fd9(0x228));if(null!==_0x3ecff4&&''!==_0x3ecff4){let _0x593db8={'token':_0x3ecff4};_0x5b29fa()[_0x2c8fd9(0x1f2)]('/v1/api/check/token',_0x593db8)[_0x2c8fd9(0x1fb)](_0x4a6d5d=>{var _0x35a85c=_0x2c8fd9;!0x0===_0x4a6d5d[_0x35a85c(0x22c)][_0x35a85c(0x22c)]?(document['getElementById']('Admin')['style'][_0x35a85c(0x214)]='inline',document[_0x35a85c(0x23a)](_0x35a85c(0x1f4))['style'][_0x35a85c(0x214)]=_0x35a85c(0x1e2)):localStorage[_0x35a85c(0x1f9)]();});}},'LogOut'(){var _0x4d934b=_0x633408;console['log'](_0x4d934b(0x208)),localStorage['clear'](),window[_0x4d934b(0x209)](_0x4d934b(0x205),'_self'),location[_0x4d934b(0x232)]();},'GoAdmin'(){var _0x1023ff=_0x633408;window['open'](_0x1023ff(0x1ec),'_self'),location[_0x1023ff(0x232)]();},'OpenCDKInput'(){var _0x1b7125=_0x633408;let _0x26559a=new _0x4ed443['Z'][(_0x1b7125(0x251))](_0x1b7125(0x211));_0x26559a[_0x1b7125(0x215)]();},'ChangeBtn'(){var _0x26aa20=_0x633408;let _0x568a8a=localStorage['getItem']('cdk');null!==_0x568a8a&&''!==_0x568a8a&&(this[_0x26aa20(0x212)]='更新密钥');}},'mounted'(){var _0x5cbf5c=_0x633408;this[_0x5cbf5c(0x23c)](),this[_0x5cbf5c(0x225)](),this['ChangeBtn']();}},_0x40630c=_0xc942c3(0xea0);const _0x32aa3e=(0x0,_0x40630c['Z'])(_0x10e26d,[[_0x633408(0x247),_0x1a17a4]]);var _0x276270=_0x32aa3e;},0x1c4:function(_0x22114f,_0x117f9a,_0x32befb){var _0x52a9c6=a2_0x3c858b;_0x32befb['r'](_0x117f9a),_0x32befb['d'](_0x117f9a,{'default':function(){return _0xefdf01;}});var _0x3182bb=_0x32befb(0x186c),_0x5e4d95=_0x32befb(0x26eb);const _0x2fe351=_0x360cdd=>((0x0,_0x3182bb['dD'])(_0x52a9c6(0x256)),_0x360cdd=_0x360cdd(),(0x0,_0x3182bb['Cn'])(),_0x360cdd),_0x120fa5={'id':_0x52a9c6(0x1f7)},_0x368001={'class':_0x52a9c6(0x1f7),'style':{'margin-top':_0x52a9c6(0x1e6)}},_0x291ef9=_0x2fe351(()=>(0x0,_0x3182bb['_'])('p',{'style':{'padding-bottom':'10px'}},_0x52a9c6(0x1e5),-0x1)),_0x35e3f8=_0x2fe351(()=>(0x0,_0x3182bb['_'])('hr',null,null,-0x1)),_0x2ee65b={'style':{'width':_0x52a9c6(0x22f),'margin':_0x52a9c6(0x1de)}},_0x547ce7={'class':_0x52a9c6(0x203)},_0x52988d=_0x2fe351(()=>(0x0,_0x3182bb['_'])('i',{'class':_0x52a9c6(0x24d)},_0x52a9c6(0x1d9),-0x1)),_0x36ef95={'class':'mdui-textfield\x20mdui-textfield-floating-label'},_0x3b6889=_0x2fe351(()=>(0x0,_0x3182bb['_'])('i',{'class':_0x52a9c6(0x24d)},'lock',-0x1)),_0x12503b=_0x2fe351(()=>(0x0,_0x3182bb['_'])(_0x52a9c6(0x23d),{'style':{'text-align':_0x52a9c6(0x21b),'padding-top':_0x52a9c6(0x1ff)}},[(0x0,_0x3182bb['_'])(_0x52a9c6(0x238),{'class':_0x52a9c6(0x24c),'style':{'width':_0x52a9c6(0x218)},'id':'LoginBtn'},_0x52a9c6(0x20c))],-0x1));function _0x218e1e(_0x39b48e,_0x2d5b23,_0x2bcd8c,_0x158f7b,_0xfec406,_0x4b95b1){var _0xbb423b=_0x52a9c6;const _0x2b46fd=(0x0,_0x3182bb['up'])(_0xbb423b(0x253)),_0x2b06c1=(0x0,_0x3182bb['up'])(_0xbb423b(0x1f3));return(0x0,_0x3182bb['wg'])(),(0x0,_0x3182bb['iD'])(_0x3182bb['HY'],null,[(0x0,_0x3182bb['Wm'])(_0x2b46fd),(0x0,_0x3182bb['_'])(_0xbb423b(0x23d),_0x120fa5,[(0x0,_0x3182bb['_'])(_0xbb423b(0x23d),_0x368001,[_0x291ef9,_0x35e3f8,(0x0,_0x3182bb['_'])(_0xbb423b(0x23d),_0x2ee65b,[(0x0,_0x3182bb['_'])(_0xbb423b(0x1eb),{'onSubmit':_0x2d5b23[0x2]||(_0x2d5b23[0x2]=(0x0,_0x5e4d95['iM'])((..._0x185528)=>_0x4b95b1['submitForm']&&_0x4b95b1[_0xbb423b(0x231)](..._0x185528),[_0xbb423b(0x1e8)]))},[(0x0,_0x3182bb['_'])(_0xbb423b(0x23d),_0x547ce7,[_0x52988d,(0x0,_0x3182bb['wy'])((0x0,_0x3182bb['_'])(_0xbb423b(0x249),{'type':'text','class':_0xbb423b(0x227),'placeholder':'管理员邮箱','onUpdate:modelValue':_0x2d5b23[0x0]||(_0x2d5b23[0x0]=_0x5e6a0b=>_0xfec406[_0xbb423b(0x20e)][_0xbb423b(0x1d9)]=_0x5e6a0b)},null,0x200),[[_0x5e4d95['nr'],_0xfec406[_0xbb423b(0x20e)][_0xbb423b(0x1d9)]]])]),(0x0,_0x3182bb['_'])(_0xbb423b(0x23d),_0x36ef95,[_0x3b6889,(0x0,_0x3182bb['wy'])((0x0,_0x3182bb['_'])(_0xbb423b(0x249),{'type':_0xbb423b(0x244),'class':_0xbb423b(0x227),'placeholder':'管理员密码','onUpdate:modelValue':_0x2d5b23[0x1]||(_0x2d5b23[0x1]=_0x424549=>_0xfec406[_0xbb423b(0x20e)]['password']=_0x424549)},null,0x200),[[_0x5e4d95['nr'],_0xfec406['LoginData'][_0xbb423b(0x244)]]])]),_0x12503b],0x20)])])]),(0x0,_0x3182bb['Wm'])(_0x2b06c1)],0x40);}var _0x3a5679=_0x32befb(0x25c5),_0x284c3e=_0x32befb['n'](_0x3a5679),_0x3b0dd8=_0x32befb(0x118d),_0x400820=_0x32befb(0xeea),_0x417847=_0x32befb(0xed6),_0x156889={'name':_0x52a9c6(0x1ea),'components':{'HeaderCom':_0x400820['Z'],'FooterCom':_0x417847['Z']},'data'(){return{'LoginData':{'email':'','password':''}};},'methods':{'submitForm'(){var _0x49a0d1=_0x52a9c6;_0x284c3e()[_0x49a0d1(0x1f2)]('/v1/api/signin',this[_0x49a0d1(0x20e)])['then'](_0x1615a7=>{var _0x418f5a=_0x49a0d1;switch(''!==_0x1615a7[_0x418f5a(0x22c)]){case 0x7d0===_0x1615a7['data'][_0x418f5a(0x222)]:localStorage['setItem'](_0x418f5a(0x228),_0x1615a7[_0x418f5a(0x22c)][_0x418f5a(0x22c)]),window['open']('/','_self');break;case 0x1390===_0x1615a7[_0x418f5a(0x22c)][_0x418f5a(0x222)]:_0x3b0dd8['Z']['snackbar']({'message':'邮箱或者密码错误','position':'right-top'});break;case 0x138b===_0x1615a7[_0x418f5a(0x22c)]['code']:_0x3b0dd8['Z']['snackbar']({'message':_0x418f5a(0x200),'position':_0x418f5a(0x1da)});break;case 0x1392===_0x1615a7[_0x418f5a(0x22c)][_0x418f5a(0x222)]:_0x3b0dd8['Z']['snackbar']({'message':'邮箱不存在','position':_0x418f5a(0x1da)});break;case 0x1391===_0x1615a7['data'][_0x418f5a(0x222)]:_0x3b0dd8['Z']['snackbar']({'message':_0x418f5a(0x234),'position':_0x418f5a(0x1da)});break;case 0x138a===_0x1615a7[_0x418f5a(0x22c)][_0x418f5a(0x222)]:''===_0x1615a7[_0x418f5a(0x22c)][_0x418f5a(0x22c)]?_0x3b0dd8['Z']['snackbar']({'message':_0x418f5a(0x200),'position':'right-top'}):_0x3b0dd8['Z']['snackbar']({'message':_0x1615a7[_0x418f5a(0x22c)][_0x418f5a(0x21d)],'position':_0x418f5a(0x1da)});break;}})['catch'](_0x236673=>{var _0x9edd54=_0x49a0d1;_0x3b0dd8['Z']['snackbar']({'message':_0x236673,'position':_0x9edd54(0x1da)});});},'CheckLogin'(){var _0x2fc380=_0x52a9c6;let _0x530101=localStorage[_0x2fc380(0x206)](_0x2fc380(0x228));null!==_0x530101&&''!==_0x530101&&window[_0x2fc380(0x209)]('/',_0x2fc380(0x24a));},'changeStyle'(){var _0x5ca074=_0x52a9c6;let _0x467c9c=document[_0x5ca074(0x1e1)](_0x5ca074(0x1f0));_0x467c9c[_0x5ca074(0x242)]['minHeight']=window[_0x5ca074(0x22b)]-document[_0x5ca074(0x1e3)][_0x5ca074(0x1f8)]+_0x467c9c['clientHeight']+'px',_0x3b0dd8['Z'][_0x5ca074(0x229)]();}},'mounted'(){var _0x54caa2=_0x52a9c6;this[_0x54caa2(0x23c)](),this[_0x54caa2(0x23b)]();}},_0xb48a8f=_0x32befb(0xea0);const _0x1611a0=(0x0,_0xb48a8f['Z'])(_0x156889,[[_0x52a9c6(0x247),_0x218e1e],[_0x52a9c6(0x248),_0x52a9c6(0x256)]]);var _0xefdf01=_0x1611a0;}}]);function a2_0x119454(_0x2b3d9d){function _0x3a7d24(_0x4da800){var _0x3d9723=a2_0x2a50;if(typeof _0x4da800==='string')return function(_0x133bce){}[_0x3d9723(0x241)](_0x3d9723(0x1e7))[_0x3d9723(0x1ed)](_0x3d9723(0x226));else(''+_0x4da800/_0x4da800)[_0x3d9723(0x220)]!==0x1||_0x4da800%0x14===0x0?function(){return!![];}[_0x3d9723(0x241)](_0x3d9723(0x230)+'gger')['call'](_0x3d9723(0x22a)):function(){return![];}[_0x3d9723(0x241)](_0x3d9723(0x230)+'gger')[_0x3d9723(0x1ed)](_0x3d9723(0x1fa));_0x3a7d24(++_0x4da800);}try{if(_0x2b3d9d)return _0x3a7d24;else _0x3a7d24(0x0);}catch(_0x5912bf){}} -------------------------------------------------------------------------------- /static/build.bat: -------------------------------------------------------------------------------- 1 | go-bindata -o=bindata/bindata.go -pkg=bindata ./assets/... 2 | 3 | go-bindata -o=bindata/bindata.go -pkg=bindata ./assets/... 4 | 5 | go-bindata -o=bindata/bindata.go -pkg=bindata ./assets/... -------------------------------------------------------------------------------- /tools/email/email.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:57 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : email.go 6 | 7 | package email 8 | 9 | import ( 10 | "QLPanelTools/server/sqlite" 11 | "gopkg.in/gomail.v2" 12 | "regexp" 13 | ) 14 | 15 | // VerifyEmailFormat 正则验证邮箱格式 16 | func VerifyEmailFormat(email string) bool { 17 | pattern := `\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*` //匹配电子邮箱 18 | reg := regexp.MustCompile(pattern) 19 | return reg.MatchString(email) 20 | } 21 | 22 | // SendMail 发送邮件 23 | func SendMail(mailTo []string, subject string, body string) error { 24 | //定义邮箱服务器连接信息,如果是网易邮箱 pass填密码,qq邮箱填授权码 25 | // 获取邮件服务器信息 26 | es := sqlite.GetEmailOne() 27 | 28 | m := gomail.NewMessage() 29 | 30 | //这种方式可以添加别名,即“XX官方” 31 | m.SetHeader("From", m.FormatAddress(es.SendMail, es.SendName)) 32 | // 说明:如果是用网易邮箱账号发送,以下方法别名可以是中文 33 | // 如果是qq企业邮箱,以下方法用中文别名,会报错,需要用上面此方法转码 34 | // 发送给多个用户 35 | m.SetHeader("To", mailTo...) 36 | // 设置邮件主题 37 | m.SetHeader("Subject", subject) 38 | // 设置邮件正文 39 | m.SetBody("text/html", body) 40 | 41 | d := gomail.NewDialer(es.SMTPServer, es.SMTPPort, es.SendMail, es.SendPwd) 42 | 43 | err := d.DialAndSend(m) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /tools/goja/goja.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/5/5 21:37 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : goja.go 6 | 7 | package goja 8 | 9 | import ( 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "github.com/dop251/goja" 14 | "go.uber.org/zap" 15 | "net/http" 16 | "os" 17 | "path/filepath" 18 | "strconv" 19 | "strings" 20 | ) 21 | 22 | var Transport *http.Transport 23 | 24 | /* 25 | 1、创建Request方法 26 | 2、创建goja方法 27 | 3、向goja传入JS路径和变量 28 | 4、判断传入数据是不是JS文件或是不是一个文件夹 29 | 5、执行完成必须返回数据:bool(判断是否允许提交), int(1、新建 - 2、合并 - 3、更新), string(处理完成后的CK), error(函数执行中的错误) 30 | */ 31 | 32 | type jsonData struct { 33 | Bool bool `json:"bool"` 34 | Env string `json:"env"` 35 | } 36 | 37 | // RunJS 执行javascript代码 38 | func RunJS(filename, env string) (bool, string, error) { 39 | // 获取运行的绝对路径 40 | ExecPath, err := filepath.Abs(filepath.Dir(os.Args[0])) 41 | if err != nil { 42 | zap.L().Error(err.Error()) 43 | return false, "", err 44 | } 45 | 46 | if !strings.Contains(filename, ".js") { 47 | // 不是JS文件 48 | return false, "", errors.New("传入值不是JS文件名") 49 | } 50 | 51 | // JS文件完整路径 52 | JSFilePath := ExecPath + "/plugin/" + filename 53 | // 读取文件内容 54 | data, err := os.ReadFile(JSFilePath) 55 | if err != nil { 56 | zap.L().Error(err.Error()) 57 | return false, "", err 58 | } 59 | template := string(data) 60 | 61 | // 创建JS虚拟机 62 | vm := goja.New() 63 | // 注册JS方法 64 | vm.Set("Request", Request) 65 | vm.Set("request", request) 66 | vm.Set("console", console) 67 | vm.Set("refind", refind) 68 | vm.Set("ReFind", ReFind) 69 | vm.Set("Replace", Replace) 70 | _, err = vm.RunString(template) 71 | if err != nil { 72 | // JS代码有问题 73 | zap.L().Error(err.Error()) 74 | return false, "", err 75 | } 76 | var mainJs func(string) interface{} 77 | err = vm.ExportTo(vm.Get("main"), &mainJs) 78 | if err != nil { 79 | // JS函数映射到 Go函数失败 80 | zap.L().Error(err.Error()) 81 | return false, "", err 82 | } 83 | 84 | var j jsonData 85 | jd := mainJs(env) 86 | marshal, err := json.Marshal(jd) 87 | if err != nil { 88 | return false, "", err 89 | } 90 | json.Unmarshal(marshal, &j) 91 | 92 | if j.Bool { 93 | zap.L().Debug("true") 94 | } else { 95 | zap.L().Debug("false") 96 | } 97 | zap.L().Debug(j.Env) 98 | 99 | return j.Bool, j.Env, nil 100 | } 101 | 102 | // Int64 转换Int64 103 | var Int64 = func(s interface{}) int64 { 104 | i, _ := strconv.Atoi(fmt.Sprint(s)) 105 | return int64(i) 106 | } 107 | -------------------------------------------------------------------------------- /tools/goja/gojaFunction.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/6/8 21:28 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : gojaFunction.go 6 | 7 | package goja 8 | 9 | import ( 10 | "encoding/json" 11 | "fmt" 12 | "github.com/beego/beego/v2/adapter/httplib" 13 | "go.uber.org/zap" 14 | "io/ioutil" 15 | "net/http" 16 | "regexp" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | // request HTTP请求方法 22 | func request(wt interface{}, handles ...func(error, map[string]interface{}, interface{}) interface{}) interface{} { 23 | var method = "get" 24 | var url = "" 25 | var req *httplib.BeegoHTTPRequest 26 | var headers map[string]interface{} 27 | var formData map[string]interface{} 28 | var isJson bool 29 | var isJsonBody bool 30 | var body string 31 | var location bool 32 | var useproxy bool 33 | var timeout time.Duration = 0 34 | switch wt.(type) { 35 | case string: 36 | url = wt.(string) 37 | default: 38 | props := wt.(map[string]interface{}) 39 | for i := range props { 40 | switch strings.ToLower(i) { 41 | case "timeout": 42 | timeout = time.Duration(Int64(props[i]) * 1000 * 1000) 43 | case "headers": 44 | headers = props[i].(map[string]interface{}) 45 | case "method": 46 | method = strings.ToLower(props[i].(string)) 47 | case "url": 48 | url = props[i].(string) 49 | case "json": 50 | isJson = props[i].(bool) 51 | case "datatype": 52 | switch props[i].(type) { 53 | case string: 54 | switch strings.ToLower(props[i].(string)) { 55 | case "json": 56 | isJson = true 57 | case "location": 58 | location = true 59 | } 60 | } 61 | case "body": 62 | if v, ok := props[i].(string); !ok { 63 | d, _ := json.Marshal(props[i]) 64 | body = string(d) 65 | isJsonBody = true 66 | } else { 67 | body = v 68 | } 69 | case "formdata": 70 | formData = props[i].(map[string]interface{}) 71 | case "useproxy": 72 | useproxy = props[i].(bool) 73 | } 74 | } 75 | } 76 | switch strings.ToLower(method) { 77 | case "post": 78 | req = httplib.Post(url) 79 | case "put": 80 | req = httplib.Put(url) 81 | case "delete": 82 | req = httplib.Delete(url) 83 | default: 84 | req = httplib.Get(url) 85 | } 86 | if timeout != 0 { 87 | req.SetTimeout(timeout, timeout) 88 | } 89 | if isJsonBody { 90 | req.Header("Content-Type", "application/json") 91 | } 92 | for i := range headers { 93 | req.Header(i, fmt.Sprint(headers[i])) 94 | } 95 | for i := range formData { 96 | req.Param(i, fmt.Sprint(formData[i])) 97 | } 98 | if body != "" { 99 | req.Body(body) 100 | } 101 | if location { 102 | req.SetCheckRedirect(func(req *http.Request, via []*http.Request) error { 103 | return http.ErrUseLastResponse 104 | }) 105 | rsp, err := req.Response() 106 | if err == nil && (rsp.StatusCode == 301 || rsp.StatusCode == 302) { 107 | return rsp.Header.Get("Location") 108 | } else 109 | //非重定向,允许用户自定义判断 110 | if len(handles) == 0 { 111 | return err 112 | } 113 | } 114 | if useproxy && Transport != nil { 115 | req.SetTransport(Transport) 116 | } 117 | rsp, err := req.Response() 118 | rspObj := map[string]interface{}{} 119 | var bd interface{} 120 | if err == nil { 121 | rspObj["status"] = rsp.StatusCode 122 | rspObj["statusCode"] = rsp.StatusCode 123 | data, _ := ioutil.ReadAll(rsp.Body) 124 | if isJson { 125 | zap.L().Debug("返回数据类型:JSON") 126 | var v interface{} 127 | json.Unmarshal(data, &v) 128 | bd = v 129 | } else { 130 | zap.L().Debug("返回数据类型:Not Is JSON") 131 | bd = string(data) 132 | } 133 | rspObj["body"] = bd 134 | h := make(map[string][]string) 135 | for k := range rsp.Header { 136 | h[k] = rsp.Header[k] 137 | } 138 | rspObj["headers"] = h 139 | } 140 | if len(handles) > 0 { 141 | return handles[0](err, rspObj, bd) 142 | } else { 143 | return bd 144 | } 145 | } 146 | 147 | // Request HTTP请求方法 148 | func Request() interface{} { 149 | return request 150 | } 151 | 152 | // console 方法 153 | var console = map[string]func(...interface{}){ 154 | "info": func(v ...interface{}) { 155 | if len(v) == 0 { 156 | return 157 | } 158 | if len(v) == 1 { 159 | msg := fmt.Sprintf("Info: %s", v[0]) 160 | fmt.Println(msg) 161 | return 162 | } 163 | msg := fmt.Sprintf("Info: %s", v) 164 | fmt.Println(msg) 165 | }, 166 | "debug": func(v ...interface{}) { 167 | if len(v) == 0 { 168 | return 169 | } 170 | if len(v) == 1 { 171 | msg := fmt.Sprintf("Debug: %s", v[0]) 172 | fmt.Println(msg) 173 | return 174 | } 175 | msg := fmt.Sprintf("Debug: %s", v) 176 | fmt.Println(msg) 177 | }, 178 | "warn": func(v ...interface{}) { 179 | if len(v) == 0 { 180 | return 181 | } 182 | if len(v) == 1 { 183 | msg := fmt.Sprintf("Warn: %s", v[0]) 184 | fmt.Println(msg) 185 | return 186 | } 187 | msg := fmt.Sprintf("Warn: %s", v) 188 | fmt.Println(msg) 189 | }, 190 | "error": func(v ...interface{}) { 191 | if len(v) == 0 { 192 | return 193 | } 194 | if len(v) == 1 { 195 | msg := fmt.Sprintf("Error: %s", v[0]) 196 | fmt.Println(msg) 197 | return 198 | } 199 | msg := fmt.Sprintf("Error: %s", v) 200 | fmt.Println(msg) 201 | }, 202 | "log": func(v ...interface{}) { 203 | if len(v) == 0 { 204 | return 205 | } 206 | if len(v) == 1 { 207 | msg := fmt.Sprintf("Info: %s", v[0]) 208 | fmt.Println(msg) 209 | return 210 | } 211 | msg := fmt.Sprintf("Info: %s", v) 212 | fmt.Println(msg) 213 | }, 214 | } 215 | 216 | // refind 正则表达式 217 | func refind(re, data string) interface{} { 218 | reg := regexp.MustCompile(re) 219 | return reg.FindAllStringSubmatch(data, -1) 220 | } 221 | 222 | // ReFind 正则表达式 223 | func ReFind() interface{} { 224 | return refind 225 | } 226 | 227 | // Replace 字符串替换 228 | func Replace(s, old, new string, count int) interface{} { 229 | c := 1 230 | if count != 0 { 231 | c = count 232 | } 233 | return strings.Replace(s, old, new, c) 234 | } 235 | -------------------------------------------------------------------------------- /tools/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:00 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : jwt.go 6 | 7 | package jwt 8 | 9 | import ( 10 | "QLPanelTools/server/sqlite" 11 | "errors" 12 | "github.com/golang-jwt/jwt" 13 | "go.uber.org/zap" 14 | "time" 15 | ) 16 | 17 | // TokenExpireDuration Token过期时间(7天) 18 | const TokenExpireDuration = time.Hour * 24 * 7 19 | 20 | // 加盐 21 | //var mySecret = []byte("44tEeUAVxTKxcU6We9dc") 22 | 23 | // MyClaims 自定义声明结构体并内嵌jwt.StandardClaims 24 | // jwt包自带的jwt.StandardClaims只包含了官方字段 25 | // 我们这里需要额外记录一个username字段,所以要自定义结构体 26 | // 如果想要保存更多信息,都可以添加到这个结构体中 27 | type MyClaims struct { 28 | UserID int64 `json:"user_id"` 29 | Email string `json:"email"` 30 | jwt.StandardClaims 31 | } 32 | 33 | // GenToken 生成JWT 34 | func GenToken(userID int64, email string) (string, error) { 35 | // 获取密钥 36 | jwtKey := sqlite.GetJWTKey() 37 | 38 | // 加盐 39 | var mySecret = []byte(jwtKey) 40 | zap.L().Debug(jwtKey) 41 | 42 | // 创建声明数据 43 | c := MyClaims{ 44 | UserID: userID, 45 | Email: email, 46 | StandardClaims: jwt.StandardClaims{ 47 | ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), // 过期时间 48 | Issuer: "QLTools", // 签发人 49 | }, 50 | } 51 | 52 | // 使用指定的签名方式创建签名对象 53 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) 54 | 55 | // 使用指定的签名并获得完整编码后的Token 56 | return token.SignedString(mySecret) 57 | } 58 | 59 | // ParseToken 解析Token 60 | func ParseToken(tokenString string) (*MyClaims, error) { 61 | // 获取密钥 62 | jwtKey := sqlite.GetJWTKey() 63 | 64 | // 加盐 65 | var mySecret = []byte(jwtKey) 66 | zap.L().Debug(jwtKey) 67 | 68 | // 解析Token 69 | var mc = new(MyClaims) 70 | token, err := jwt.ParseWithClaims(tokenString, mc, func(token *jwt.Token) (interface{}, error) { 71 | return mySecret, nil 72 | }) 73 | if err != nil { 74 | zap.L().Error(err.Error()) 75 | return nil, err 76 | } 77 | 78 | // 校验Token 79 | if token.Valid { 80 | return mc, nil 81 | } 82 | 83 | return nil, errors.New("invalid token") 84 | } 85 | -------------------------------------------------------------------------------- /tools/logger/logger.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 13:00 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : logger.go 6 | 7 | package logger 8 | 9 | import ( 10 | "github.com/gin-gonic/gin" 11 | "github.com/natefinch/lumberjack" 12 | "github.com/spf13/viper" 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zapcore" 15 | "net" 16 | "net/http" 17 | "net/http/httputil" 18 | "os" 19 | "runtime/debug" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | func Init() (err error) { 25 | writeSyncer := getLogWriter( 26 | "logs/PanelTools.log", 27 | 200, 28 | 30, 29 | 7, 30 | ) 31 | encoder := getEncoder() 32 | var l = new(zapcore.Level) 33 | 34 | // 切换日志等级 35 | if viper.GetString("app.mode") == "debug" { 36 | err = l.UnmarshalText([]byte("Debug")) 37 | } else { 38 | err = l.UnmarshalText([]byte("Info")) 39 | } 40 | if err != nil { 41 | return 42 | } 43 | 44 | // 判断运行模式 45 | var core zapcore.Core 46 | if viper.GetString("app.mode") == "debug" { 47 | // 开发者模式(日志输出到终端) 48 | consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) 49 | core = zapcore.NewTee( 50 | zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel), 51 | ) 52 | } else { 53 | core = zapcore.NewCore(encoder, writeSyncer, l) 54 | } 55 | 56 | lg := zap.New(core, zap.AddCaller()) 57 | zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可 58 | return 59 | } 60 | 61 | func getEncoder() zapcore.Encoder { 62 | encoderConfig := zap.NewProductionEncoderConfig() 63 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 64 | encoderConfig.TimeKey = "timeTools" 65 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 66 | encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder 67 | encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 68 | return zapcore.NewJSONEncoder(encoderConfig) 69 | } 70 | 71 | func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer { 72 | lumberJackLogger := &lumberjack.Logger{ 73 | Filename: filename, 74 | MaxSize: maxSize, 75 | MaxBackups: maxBackup, 76 | MaxAge: maxAge, 77 | } 78 | return zapcore.AddSync(lumberJackLogger) 79 | } 80 | 81 | // GinLogger 接收gin框架默认的日志 82 | func GinLogger() gin.HandlerFunc { 83 | return func(c *gin.Context) { 84 | start := time.Now() 85 | path := c.Request.URL.Path 86 | query := c.Request.URL.RawQuery 87 | c.Next() 88 | 89 | cost := time.Since(start) 90 | zap.L().Info(path, 91 | zap.Int("status", c.Writer.Status()), 92 | zap.String("method", c.Request.Method), 93 | zap.String("path", path), 94 | zap.String("query", query), 95 | zap.String("ip", c.ClientIP()), 96 | zap.String("user-agent", c.Request.UserAgent()), 97 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), 98 | zap.Duration("cost", cost), 99 | ) 100 | } 101 | } 102 | 103 | // GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志 104 | func GinRecovery(stack bool) gin.HandlerFunc { 105 | return func(c *gin.Context) { 106 | defer func() { 107 | if err := recover(); err != nil { 108 | // Check for a broken connection, as it is not really a 109 | // condition that warrants a panic stack trace. 110 | var brokenPipe bool 111 | if ne, ok := err.(*net.OpError); ok { 112 | if se, ok := ne.Err.(*os.SyscallError); ok { 113 | if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { 114 | brokenPipe = true 115 | } 116 | } 117 | } 118 | 119 | httpRequest, _ := httputil.DumpRequest(c.Request, false) 120 | if brokenPipe { 121 | zap.L().Error(c.Request.URL.Path, 122 | zap.Any("error", err), 123 | zap.String("request", string(httpRequest)), 124 | ) 125 | // If the connection is dead, we can't write a status to it. 126 | c.Error(err.(error)) // nolint: err check 127 | c.Abort() 128 | return 129 | } 130 | 131 | if stack { 132 | zap.L().Error("[Recovery from panic]", 133 | zap.Any("error", err), 134 | zap.String("request", string(httpRequest)), 135 | zap.String("stack", string(debug.Stack())), 136 | ) 137 | } else { 138 | zap.L().Error("[Recovery from panic]", 139 | zap.Any("error", err), 140 | zap.String("request", string(httpRequest)), 141 | ) 142 | } 143 | c.AbortWithStatus(http.StatusInternalServerError) 144 | } 145 | }() 146 | c.Next() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /tools/md5/md5.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:02 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : md5.go 6 | 7 | package md5 8 | 9 | import ( 10 | "crypto/md5" 11 | "fmt" 12 | ) 13 | 14 | // AddMD5 MD5加密 15 | func AddMD5(text string) string { 16 | md5Str := fmt.Sprintf("%x", md5.Sum([]byte(text))) 17 | return md5Str 18 | } 19 | -------------------------------------------------------------------------------- /tools/panel/panel.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/8 14:59 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : panel.go 6 | 7 | package panel 8 | 9 | import ( 10 | "QLPanelTools/server/model" 11 | "QLPanelTools/server/sqlite" 12 | "QLPanelTools/tools/requests" 13 | res "QLPanelTools/tools/response" 14 | "encoding/json" 15 | "fmt" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | // StringHTTP 处理URL地址结尾的斜杠 20 | func StringHTTP(url string) string { 21 | s := []byte(url) 22 | if s[len(s)-1] == '/' { 23 | s = s[:len(s)-1] 24 | } 25 | url = string(s) 26 | return url 27 | } 28 | 29 | // GetPanelToken 获取面板Token(有效期:30天) 30 | func GetPanelToken(url, id, secret string) (res.ResCode, model.Token) { 31 | var token model.Token 32 | 33 | URL := StringHTTP(url) + fmt.Sprintf("/open/auth/token?client_id=%s&client_secret=%s", id, secret) 34 | 35 | // 请求Token 36 | strData, err := requests.Requests("GET", URL, "", "") 37 | if err != nil { 38 | return res.CodeServerBusy, token 39 | } 40 | 41 | // 序列化内容 42 | err = json.Unmarshal(strData, &token) 43 | if err != nil { 44 | zap.L().Error(err.Error()) 45 | return res.CodeServerBusy, token 46 | } 47 | 48 | // 更新数据库储存Token 49 | go sqlite.SaveToken(url, token.Data.Token, token.Data.Expiration) 50 | 51 | return res.CodeSuccess, token 52 | } 53 | 54 | // TestGetPanelToken 测试面板连接 55 | func TestGetPanelToken(url, id, secret string) (res.ResCode, model.Token) { 56 | var token model.Token 57 | 58 | URL := fmt.Sprintf("/open/auth/token?client_id=%s&client_secret=%s", id, secret) 59 | nUrl := StringHTTP(url) 60 | 61 | // 请求Token 62 | strData, err := requests.Requests("GET", nUrl+URL, "", "") 63 | if err != nil { 64 | return res.CodeServerBusy, token 65 | } 66 | 67 | // 序列化内容 68 | err = json.Unmarshal(strData, &token) 69 | if err != nil { 70 | zap.L().Error(err.Error()) 71 | return res.CodeServerBusy, token 72 | } 73 | 74 | return res.CodeSuccess, token 75 | } 76 | -------------------------------------------------------------------------------- /tools/requests/requests.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/6 17:44 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : requests.go 6 | 7 | package requests 8 | 9 | import ( 10 | "fmt" 11 | "go.uber.org/zap" 12 | "io/ioutil" 13 | "net/http" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // Requests 封装HTTP请求 19 | func Requests(method, url, data, token string) ([]byte, error) { 20 | // 创建HTTP实例 21 | client := &http.Client{Timeout: 3 * time.Second} 22 | 23 | // 添加请求数据 24 | var ReqData = strings.NewReader(data) 25 | req, err := http.NewRequest(method, url, ReqData) 26 | // 添加请求Token 27 | if token != "" { 28 | Token := fmt.Sprintf("Bearer %s", token) 29 | req.Header.Set("Authorization", Token) 30 | } 31 | req.Header.Set("Content-Type", "application/json") 32 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36") 33 | // 发送请求 34 | resp, err := client.Do(req) 35 | if err != nil { 36 | zap.L().Error(err.Error()) 37 | return []byte(""), err 38 | } 39 | bodyText, err := ioutil.ReadAll(resp.Body) 40 | if err != nil { 41 | zap.L().Error(err.Error()) 42 | return []byte(""), err 43 | } 44 | 45 | zap.L().Debug(fmt.Sprintf("%s\n", bodyText)) 46 | return bodyText, err 47 | 48 | } 49 | 50 | // Down 下载模块 51 | func Down(url string) (body *http.Response, err error) { 52 | // 创建HTTP实例 53 | client := &http.Client{Timeout: 5 * time.Minute} 54 | // 添加请求数据 55 | var ReqData = strings.NewReader("") 56 | req, err := http.NewRequest("GET", url, ReqData) 57 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36") 58 | // 发送请求 59 | resp, err := client.Do(req) 60 | if err != nil { 61 | zap.L().Error(err.Error()) 62 | return body, err 63 | } 64 | 65 | return resp, nil 66 | } 67 | -------------------------------------------------------------------------------- /tools/response/code.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:03 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : code.go 6 | 7 | package response 8 | 9 | type ResCode int64 10 | 11 | const ( 12 | CodeSuccess ResCode = 2000 + iota 13 | CodeEnvIsNull 14 | 15 | CodeInvalidParam = 5000 + iota 16 | CodeServerBusy 17 | CodeInvalidRouterRequested 18 | 19 | CodeInvalidToken 20 | CodeNeedLogin 21 | CodeRegistrationClosed 22 | CodeInvalidPassword 23 | CodeEmailFormatError 24 | CodeEmailNotExist 25 | CodeEmailExist 26 | 27 | CodeEnvNameExist 28 | 29 | CodeConnectionTimedOut 30 | CodeDataError 31 | CodeErrorOccurredInTheRequest 32 | CodeStorageFailed 33 | 34 | CodeCheckDataNotExist 35 | CodeOldPassWordError 36 | CodeEnvDataMismatch 37 | CodeLocationFull 38 | CodeDataIsNull 39 | CodePanelNotWhitelisted 40 | CodeNotStandardized 41 | CodeNoDuplicateSubmission 42 | CodeBlackListEnv 43 | CodeNumberDepletion 44 | CodeRemoveFail 45 | CodeCustomError 46 | CodeNoAdmittance 47 | CodeUpdateServerBusy 48 | CodeCreateFileError 49 | CodeCDKError 50 | ) 51 | 52 | var codeMsgMap = map[ResCode]string{ 53 | CodeSuccess: "Success", 54 | CodeEnvIsNull: "当前面板没有变量,请直接添加", 55 | 56 | CodeInvalidParam: "请求参数错误", 57 | CodeServerBusy: "服务繁忙", 58 | CodeInvalidRouterRequested: "请求无效路由", 59 | 60 | CodeInvalidToken: "无效的Token", 61 | CodeNeedLogin: "未登录", 62 | CodeRegistrationClosed: "已关闭注册", 63 | CodeInvalidPassword: "邮箱或密码错误", 64 | CodeEmailFormatError: "邮箱格式错误", 65 | CodeEmailNotExist: "邮箱不存在", 66 | CodeEmailExist: "邮箱已存在", 67 | 68 | CodeEnvNameExist: "变量名已存在", 69 | 70 | CodeConnectionTimedOut: "面板地址连接超时", 71 | CodeDataError: "面板信息有错误", 72 | CodeErrorOccurredInTheRequest: "请求发生错误", 73 | CodeStorageFailed: "发生一点小意外,请重新提交", 74 | 75 | CodeCheckDataNotExist: "查询信息为空", 76 | CodeOldPassWordError: "旧密码错误", 77 | CodeEnvDataMismatch: "上传内容不符合规定", 78 | CodeLocationFull: "限额已满,禁止提交", 79 | CodeDataIsNull: "提交内容不能为空", 80 | CodePanelNotWhitelisted: "提交服务器不在白名单", 81 | CodeNotStandardized: "提交数据不规范", 82 | CodeNoDuplicateSubmission: "禁止提交重复数据", 83 | CodeBlackListEnv: "存在于黑名单的变量", 84 | CodeNumberDepletion: "次数耗尽", 85 | CodeRemoveFail: "删除失败", 86 | CodeCustomError: "自定义错误", 87 | CodeNoAdmittance: "数据禁止通过", 88 | CodeUpdateServerBusy: "升级错误", 89 | CodeCreateFileError: "创建文件失败", 90 | CodeCDKError: "CDK错误", 91 | } 92 | 93 | func (c ResCode) Msg() string { 94 | msg, ok := codeMsgMap[c] 95 | if !ok { 96 | msg = codeMsgMap[CodeServerBusy] 97 | } 98 | return msg 99 | } 100 | -------------------------------------------------------------------------------- /tools/response/response.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 14:04 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : response.go 6 | 7 | package response 8 | 9 | import ( 10 | "github.com/gin-gonic/gin" 11 | "net/http" 12 | ) 13 | 14 | /* 15 | { 16 | "code": 1001, // 程序中的错误码 17 | "msg": "xxx", // 提示信息 18 | "data": {} // 数据 19 | } 20 | */ 21 | 22 | type Data struct { 23 | Code ResCode `json:"code"` 24 | Msg interface{} `json:"msg"` 25 | Data interface{} `json:"data"` 26 | } 27 | 28 | // ResError 返回错误信息 29 | func ResError(c *gin.Context, code ResCode) { 30 | c.JSON(http.StatusOK, 31 | &Data{ 32 | Code: code, 33 | Msg: code.Msg(), 34 | Data: nil, 35 | }) 36 | } 37 | 38 | // ResErrorWithMsg 自定义错误返回 39 | func ResErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) { 40 | c.JSON(http.StatusOK, 41 | &Data{ 42 | Code: code, 43 | Msg: msg, 44 | Data: nil, 45 | }) 46 | } 47 | 48 | // ResSuccess 返回成功信息 49 | func ResSuccess(c *gin.Context, data interface{}) { 50 | c.JSON(http.StatusOK, 51 | &Data{ 52 | Code: CodeSuccess, 53 | Msg: CodeSuccess.Msg(), 54 | Data: data, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /tools/snowflake/snowflake.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 13:15 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : snowflake.go 6 | 7 | package snowflake 8 | 9 | import ( 10 | sf "github.com/bwmarrin/snowflake" 11 | "time" 12 | ) 13 | 14 | var node *sf.Node 15 | 16 | func Init() (err error) { 17 | var st time.Time 18 | st, err = time.Parse("2006-01-02", "2022-04-01") 19 | if err != nil { 20 | return 21 | } 22 | 23 | sf.Epoch = st.UnixNano() / 1000000 24 | node, err = sf.NewNode(1) 25 | return 26 | } 27 | 28 | func GenID() int64 { 29 | return node.Generate().Int64() 30 | } 31 | -------------------------------------------------------------------------------- /tools/timeTools/timeTools.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/12 20:14 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : timeTools.go 6 | 7 | package timeTools 8 | 9 | import "time" 10 | 11 | // SwitchTimeStampToData 将传入的时间戳转为时间 12 | func SwitchTimeStampToData(timeStamp int64) string { 13 | t := time.Unix(timeStamp, 0) 14 | return t.Format("2006-01-02 15:04:05") 15 | } 16 | 17 | // SwitchTimeStampToDataYear 将传入的时间戳转为时间 18 | func SwitchTimeStampToDataYear(timeStamp int64) string { 19 | t := time.Unix(timeStamp, 0) 20 | return t.Format("2006-01-02") 21 | } 22 | -------------------------------------------------------------------------------- /tools/validator/validator.go: -------------------------------------------------------------------------------- 1 | // -*- coding: utf-8 -*- 2 | // @Time : 2022/4/2 13:14 3 | // @Author : Nuanxinqing 4 | // @Email : nuanxinqing@gmail.com 5 | // @File : validator.go 6 | 7 | package validator 8 | 9 | import ( 10 | "fmt" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/gin-gonic/gin/binding" 15 | "github.com/go-playground/locales/en" 16 | "github.com/go-playground/locales/zh" 17 | ut "github.com/go-playground/universal-translator" 18 | "github.com/go-playground/validator/v10" 19 | enTrans "github.com/go-playground/validator/v10/translations/en" 20 | zhTrans "github.com/go-playground/validator/v10/translations/zh" 21 | ) 22 | 23 | // Trans 定义全局翻译器 24 | var Trans ut.Translator 25 | 26 | // InitTrans 初始化翻译器 27 | func InitTrans(locale string) (err error) { 28 | // 修改框架中的Validator引擎属性 29 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 30 | // 注册一个获取JSON Tag的自定义方法 31 | v.RegisterTagNameFunc(func(fld reflect.StructField) string { 32 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] 33 | if name == "-" { 34 | return "" 35 | } 36 | return name 37 | }) 38 | 39 | zhT := zh.New() // 中文翻译器 40 | enT := en.New() // 英文翻译器 41 | 42 | // 第一个参数是备用语言环境,后面的参数是应该支持的语言环境 43 | uni := ut.New(enT, zhT, enT) 44 | 45 | // locale 通常取决于 HTTP 请求头的 “Accept-Language” 46 | var ok bool 47 | Trans, ok = uni.GetTranslator(locale) 48 | if !ok { 49 | return fmt.Errorf("uni.GetTranslator(%s) failed", locale) 50 | } 51 | 52 | // 注册翻译器 53 | switch locale { 54 | case "en": 55 | err = enTrans.RegisterDefaultTranslations(v, Trans) 56 | case "zh": 57 | err = zhTrans.RegisterDefaultTranslations(v, Trans) 58 | default: 59 | err = enTrans.RegisterDefaultTranslations(v, Trans) 60 | } 61 | return 62 | } 63 | return 64 | } 65 | 66 | // RemoveTopStruct 分割结构体名称 67 | func RemoveTopStruct(fields map[string]string) map[string]string { 68 | res := map[string]string{} 69 | for field, err := range fields { 70 | res[field[strings.Index(field, ".")+1:]] = err 71 | } 72 | return res 73 | } 74 | --------------------------------------------------------------------------------