├── .dockerignore ├── .gitattributes ├── .github ├── image │ ├── add_site.png │ ├── category.png │ ├── config.png │ ├── index.png │ └── site.png └── workflows │ ├── docker-image.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api └── v1 │ ├── category.go │ ├── config.go │ ├── dashboard.go │ ├── errors.go │ ├── index.go │ ├── site.go │ ├── user.go │ └── v1.go ├── cmd └── server │ ├── main.go │ └── wire │ ├── wire.go │ └── wire_gen.go ├── config ├── local.yml ├── prod.yml └── test.yml ├── docs ├── docs.go ├── swagger.json └── swagger.yaml ├── go.mod ├── go.sum ├── internal ├── dal │ ├── model │ │ ├── st_category.gen.go │ │ ├── st_site.gen.go │ │ ├── sys_config.gen.go │ │ ├── sys_menu.gen.go │ │ ├── sys_user.gen.go │ │ └── sys_user_menu.gen.go │ ├── query │ │ ├── gen.go │ │ ├── st_category.gen.go │ │ ├── st_site.gen.go │ │ ├── sys_config.gen.go │ │ ├── sys_menu.gen.go │ │ ├── sys_user.gen.go │ │ └── sys_user_menu.gen.go │ └── repository │ │ ├── repository.go │ │ ├── st_category.gen.go │ │ ├── st_category.go │ │ ├── st_category.mockgen.go │ │ ├── st_site.gen.go │ │ ├── st_site.go │ │ ├── st_site.mockgen.go │ │ ├── sys_config.gen.go │ │ ├── sys_config.go │ │ ├── sys_config.mockgen.go │ │ ├── sys_menu.gen.go │ │ ├── sys_menu.go │ │ ├── sys_menu.mockgen.go │ │ ├── sys_user.gen.go │ │ ├── sys_user.go │ │ ├── sys_user.mockgen.go │ │ ├── sys_user_menu.gen.go │ │ ├── sys_user_menu.go │ │ └── sys_user_menu.mockgen.go ├── handler │ ├── category │ │ ├── create.go │ │ ├── delete.go │ │ ├── detail.go │ │ ├── handler.go │ │ ├── handler_test.go │ │ ├── list.go │ │ └── update.go │ ├── config │ │ ├── config.go │ │ ├── handler.go │ │ ├── handler_test.go │ │ └── update.go │ ├── dashboard │ │ ├── dashboard.go │ │ └── handler.go │ ├── handler.go │ ├── handler_test.go │ ├── index │ │ ├── about.go │ │ ├── handler.go │ │ └── index.go │ ├── site │ │ ├── create.go │ │ ├── delete.go │ │ ├── export.go │ │ ├── handler.go │ │ ├── handler_test.go │ │ ├── list.go │ │ ├── sync.go │ │ └── update.go │ └── user │ │ ├── handler.go │ │ ├── handler_test.go │ │ ├── info.go │ │ ├── login.go │ │ ├── logout.go │ │ └── updatepassword.go ├── middleware │ ├── cors.go │ ├── jwt.go │ └── log.go ├── server │ └── http.go └── service │ ├── category │ ├── create.go │ ├── delete.go │ ├── detail.go │ ├── list.go │ ├── service.go │ ├── service.mockgen.go │ ├── service_test.go │ └── update.go │ ├── config │ ├── config.go │ ├── service.go │ ├── service.mockgen.go │ ├── service_test.go │ └── update.go │ ├── dashboard │ ├── dashboard.go │ ├── service.go │ ├── service.mockgen.go │ └── service_test.go │ ├── index │ ├── Index.go │ ├── about.go │ ├── service.go │ ├── service.mockgen.go │ └── service_test.go │ ├── service.go │ ├── site │ ├── batchcreate.go │ ├── delete.go │ ├── export.go │ ├── list.go │ ├── service.go │ ├── service.mockgen.go │ ├── service_test.go │ ├── sync.go │ └── update.go │ └── user │ ├── info.go │ ├── login.go │ ├── service.go │ ├── service.mockgen.go │ ├── service_test.go │ └── updatepassword.go ├── pkg ├── app │ └── app.go ├── config │ └── config.go ├── gormx │ ├── db.go │ └── field.go ├── jwt │ └── jwt.go ├── log │ └── log.go ├── server │ ├── http │ │ └── http.go │ └── server.go ├── sid │ ├── convert.go │ └── sid.go ├── tools │ ├── besticon.go │ ├── colly.go │ ├── img.go │ ├── useragent.go │ └── work.go └── zapgorm2 │ └── zapgorm2.go ├── storage └── .gitkeep └── web ├── assets.go ├── static ├── admin │ ├── css │ │ ├── animate.min.css │ │ ├── bootstrap.min.css │ │ ├── materialdesignicons.min.css │ │ └── style.min.css │ ├── fonts │ │ ├── materialdesignicons-webfont.eot │ │ ├── materialdesignicons-webfont.ttf │ │ ├── materialdesignicons-webfont.woff │ │ └── materialdesignicons-webfont.woff2 │ ├── image │ │ └── gr-code.png │ └── js │ │ ├── authorization │ │ ├── crypto-js.min.js │ │ ├── enc-base64.min.js │ │ ├── hmac-sha256.js │ │ ├── ksort.js │ │ └── md5.min.js │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.min.js │ │ ├── httpclient.js │ │ ├── index.min.js │ │ ├── jquery.cookie.min.js │ │ ├── jquery.min.js │ │ ├── jquery.pagination.js │ │ ├── perfect-scrollbar.min.js │ │ ├── popper.min.js │ │ └── utils.js ├── index │ ├── css │ │ ├── bootstrap.css │ │ ├── fonts │ │ │ ├── elusive │ │ │ │ ├── css │ │ │ │ │ ├── animation.css │ │ │ │ │ ├── elusive-codes.css │ │ │ │ │ ├── elusive-embedded.css │ │ │ │ │ ├── elusive-ie7-codes.css │ │ │ │ │ ├── elusive-ie7.css │ │ │ │ │ └── elusive.css │ │ │ │ └── font │ │ │ │ │ ├── elusive.eot │ │ │ │ │ ├── elusive.svg │ │ │ │ │ ├── elusive.ttf │ │ │ │ │ └── elusive.woff │ │ │ ├── fontawesome │ │ │ │ ├── css │ │ │ │ │ ├── font-awesome.css │ │ │ │ │ └── font-awesome.min.css │ │ │ │ └── fonts │ │ │ │ │ ├── FontAwesome.otf │ │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ │ └── fontawesome-webfont.woff │ │ │ ├── glyphicons │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ └── glyphicons-halflings-regular.woff │ │ │ ├── linecons │ │ │ │ ├── css │ │ │ │ │ ├── animation.css │ │ │ │ │ ├── linecons-codes.css │ │ │ │ │ ├── linecons-embedded.css │ │ │ │ │ ├── linecons-ie7-codes.css │ │ │ │ │ ├── linecons-ie7.css │ │ │ │ │ └── linecons.css │ │ │ │ └── font │ │ │ │ │ ├── linecons.eot │ │ │ │ │ ├── linecons.svg │ │ │ │ │ ├── linecons.ttf │ │ │ │ │ └── linecons.woff │ │ │ └── meteocons │ │ │ │ ├── css │ │ │ │ ├── animation.css │ │ │ │ ├── meteocons-codes.css │ │ │ │ ├── meteocons-embedded.css │ │ │ │ ├── meteocons-ie7-codes.css │ │ │ │ ├── meteocons-ie7.css │ │ │ │ └── meteocons.css │ │ │ │ └── font │ │ │ │ ├── meteocons.eot │ │ │ │ ├── meteocons.svg │ │ │ │ ├── meteocons.ttf │ │ │ │ └── meteocons.woff │ │ ├── hclonely.css │ │ ├── nav.css │ │ ├── xenon-components.css │ │ ├── xenon-core.css │ │ ├── xenon-forms.css │ │ ├── xenon-skins.css │ │ └── xenon.css │ ├── images │ │ ├── favicon.png │ │ ├── flags │ │ │ ├── flag-cn.png │ │ │ └── flag-us.png │ │ ├── logo-collapsed@2x.png │ │ ├── logo@2x.png │ │ ├── logo_dark@2x.png │ │ ├── off_on.png │ │ └── search_icon.png │ ├── js │ │ ├── TweenMax.min.js │ │ ├── bootstrap.min.js │ │ ├── header.js │ │ ├── joinable.js │ │ ├── jquery-1.11.1.min.js │ │ ├── lozad.js │ │ ├── resizeable.js │ │ ├── xenon-api.js │ │ ├── xenon-custom.js │ │ └── xenon-toggles.js │ └── webstack_logos.sketch └── plugin │ ├── bootstrap-fileinput │ ├── css │ │ └── fileinput.min.css │ └── js │ │ ├── fileinput.min.js │ │ └── zh.min.js │ ├── bootstrap-markdown-editor │ ├── css │ │ └── bootstrap-markdown-editor.min.css │ └── js │ │ ├── ace │ │ ├── ace.js │ │ ├── ext-language_tools.js │ │ ├── mode-markdown.js │ │ └── theme-tomorrow.js │ │ ├── bootstrap-markdown-editor.min.js │ │ ├── bootstrap-markdown-editor.min.js.map │ │ └── marked.min.js │ ├── bootstrap-maxlength │ └── bootstrap-maxlength.min.js │ ├── bootstrap-multitabs │ ├── multitabs.js │ ├── multitabs.min.css │ └── multitabs.min.js │ ├── bootstrap-select │ ├── bootstrap-select.css │ ├── bootstrap-select.min.css │ ├── bootstrap-select.min.js │ └── i18n │ │ ├── defaults-zh_CN.min.js │ │ └── defaults-zh_TW.min.js │ ├── bootstrap-table │ ├── bootstrap-table.css │ ├── bootstrap-table.js │ ├── bootstrap-table.min.css │ ├── bootstrap-table.min.js │ ├── extensions │ │ ├── accent-neutralise │ │ │ └── bootstrap-table-accent-neutralise.min.js │ │ ├── addrbar │ │ │ └── bootstrap-table-addrbar.min.js │ │ ├── auto-refresh │ │ │ └── bootstrap-table-auto-refresh.min.js │ │ ├── cell-input │ │ │ ├── bootstrap-table-cell-input.min.css │ │ │ └── bootstrap-table-cell-input.min.js │ │ ├── cookie │ │ │ └── bootstrap-table-cookie.min.js │ │ ├── copy-rows │ │ │ └── bootstrap-table-copy-rows.min.js │ │ ├── defer-url │ │ │ └── bootstrap-table-defer-url.min.js │ │ ├── editable │ │ │ └── bootstrap-table-editable.min.js │ │ ├── export │ │ │ └── bootstrap-table-export.min.js │ │ ├── filter-control │ │ │ ├── bootstrap-table-filter-control.min.css │ │ │ └── bootstrap-table-filter-control.min.js │ │ ├── fixed-columns │ │ │ ├── bootstrap-table-fixed-columns.js │ │ │ └── bootstrap-table-fixed-columns.scss │ │ ├── group-by-v2 │ │ │ ├── bootstrap-table-group-by.js │ │ │ ├── bootstrap-table-group-by.scss │ │ │ └── extension.json │ │ ├── i18n-enhance │ │ │ ├── bootstrap-table-i18n-enhance.js │ │ │ └── extension.json │ │ ├── key-events │ │ │ ├── bootstrap-table-key-events.js │ │ │ └── extension.json │ │ ├── mobile │ │ │ ├── bootstrap-table-mobile.js │ │ │ └── extension.json │ │ ├── multiple-sort │ │ │ ├── bootstrap-table-multiple-sort.js │ │ │ └── extension.json │ │ ├── page-jump-to │ │ │ ├── bootstrap-table-page-jump-to.js │ │ │ └── bootstrap-table-page-jump-to.scss │ │ ├── pipeline │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── bootstrap-table-pipeline.js │ │ │ └── extension.json │ │ ├── print │ │ │ └── bootstrap-table-print.js │ │ ├── reorder-columns │ │ │ ├── bootstrap-table-reorder-columns.js │ │ │ └── extension.json │ │ ├── reorder-rows │ │ │ ├── bootstrap-table-reorder-rows.js │ │ │ ├── bootstrap-table-reorder-rows.scss │ │ │ └── extension.json │ │ ├── resizable │ │ │ ├── bootstrap-table-resizable.js │ │ │ └── extension.json │ │ ├── sticky-header │ │ │ ├── bootstrap-table-sticky-header.js │ │ │ ├── bootstrap-table-sticky-header.scss │ │ │ └── extension.json │ │ ├── toolbar │ │ │ ├── bootstrap-table-toolbar.js │ │ │ └── extension.json │ │ └── treegrid │ │ │ └── bootstrap-table-treegrid.min.js │ └── locale │ │ ├── bootstrap-table-zh-CN.min.js │ │ └── bootstrap-table-zh-TW.min.js │ ├── jquery-confirm │ ├── jquery-confirm.min.css │ └── jquery-confirm.min.js │ └── jquery-treegrid │ ├── jquery.treegrid.min.css │ └── jquery.treegrid.min.js ├── templates ├── admin │ ├── admin_index.html │ ├── admin_login.html │ └── admin_modify_password.html ├── category │ └── category_view.html ├── config │ └── conf_web.html ├── dashboard │ └── dashboard.html ├── index │ ├── 404.html │ ├── about.html │ └── index.html └── site │ ├── site_add.html │ └── site_list.html └── upload └── favicon.png /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .idea -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | # Force the following filetypes to have unix eols, so Windows does not break them 4 | *.* text eol=lf 5 | 6 | # Windows forced line-endings 7 | /.idea/* text eol=crlf 8 | 9 | # 10 | ## These files are binary and should be left untouched 11 | # 12 | 13 | # (binary is a macro for -text -diff) 14 | *.png binary 15 | -------------------------------------------------------------------------------- /.github/image/add_site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/.github/image/add_site.png -------------------------------------------------------------------------------- /.github/image/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/.github/image/category.png -------------------------------------------------------------------------------- /.github/image/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/.github/image/config.png -------------------------------------------------------------------------------- /.github/image/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/.github/image/index.png -------------------------------------------------------------------------------- /.github/image/site.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/.github/image/site.png -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Multi-Arch Docker Image 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - v* 9 | paths-ignore: 10 | - '.*' 11 | - '.*/workflows/**' 12 | - 'README*' 13 | - '**/README*' 14 | pull_request: 15 | branches: 16 | - main 17 | paths-ignore: 18 | - '.*' 19 | - '.*/workflows/**' 20 | - 'README*' 21 | - '**/README*' 22 | 23 | env: 24 | DOCKER_IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME }} # 可配置的镜像名称 25 | DOCKER_PLATFORMS: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} # PR 时单平台,Merge 时多平台 26 | 27 | jobs: 28 | build: 29 | runs-on: ubuntu-latest 30 | 31 | steps: 32 | # 检出代码 33 | - name: Checkout code 34 | uses: actions/checkout@v3 35 | 36 | # 设置 QEMU 以支持跨架构构建 37 | - name: Set up QEMU 38 | uses: docker/setup-qemu-action@v2 39 | 40 | # 设置 Docker Buildx 41 | - name: Set up Docker Buildx 42 | uses: docker/setup-buildx-action@v2 43 | 44 | # 登录到 Docker Hub(仅在 push 事件时登录) 45 | - name: Log in to Docker Hub 46 | if: github.event_name == 'push' # 只有 push 事件时登录 47 | uses: docker/login-action@v2 48 | with: 49 | username: ${{ secrets.DOCKER_USERNAME }} 50 | password: ${{ secrets.DOCKER_PASSWORD }} 51 | 52 | # 设置动态标签 53 | - name: Set Docker Image Tag 54 | id: set_tag 55 | run: | 56 | if [ "${{ github.event_name }}" == "pull_request" ]; then 57 | echo "DOCKER_IMAGE_TAG=pr-${{ github.event.number }}" >> $GITHUB_ENV 58 | elif [ "${{ github.event_name }}" == "push" ]; then 59 | if [[ "${{ github.ref }}" == refs/tags/* ]]; then 60 | echo "DOCKER_IMAGE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV # 使用 Git 标签 61 | else 62 | echo "DOCKER_IMAGE_TAG=latest" >> $GITHUB_ENV # 默认使用 latest 63 | fi 64 | fi 65 | 66 | # 构建镜像 67 | - name: Build Docker Image 68 | uses: docker/build-push-action@v4 69 | with: 70 | context: . 71 | platforms: ${{ env.DOCKER_PLATFORMS }} # 动态设置平台 72 | tags: | 73 | ${{ env.DOCKER_IMAGE_NAME }}:${{ env.DOCKER_IMAGE_TAG }} 74 | ${{ env.DOCKER_IMAGE_NAME }}:latest 75 | load: ${{ github.event_name != 'push' }} # 仅在非 push 事件时加载到本地 76 | push: ${{ github.event_name == 'push' }} # 仅在 push 事件时推送 77 | cache-from: type=gha # 使用 GitHub Actions 缓存 78 | cache-to: type=gha,mode=max # 缓存 Docker 层以加快构建速度 79 | build-args: | 80 | APP_RELATIVE_PATH=./cmd/server 81 | APP_CONF=config/prod.yml -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: Release on GitHub 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # 检出代码 15 | - name: Check out code 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 # 获取完整的 Git 历史 19 | 20 | # 设置 Go 环境 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: 1.22 25 | cache: true # 启用 Go 模块缓存 26 | 27 | # 运行 GoReleaser 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v4 30 | with: 31 | version: latest 32 | args: release --clean --skip=validate 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | storage/logs 3 | .idea 4 | *.log 5 | deploy/docker-compose/conf 6 | deploy/docker-compose/data 7 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | project_name: "webstack-go" 4 | 5 | builds: 6 | - id: default 7 | dir: . # 根目录 8 | main: ./cmd/server # 指定 main 包的路径 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - amd64 15 | - arm64 16 | env: 17 | - CGO_ENABLED=0 18 | flags: 19 | - -trimpath 20 | ldflags: 21 | - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 22 | 23 | archives: 24 | - id: default 25 | format: tar.gz 26 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 27 | files: 28 | - src: "config/*" 29 | dst: "config" 30 | wrap_in_directory: true 31 | format_overrides: 32 | - goos: windows 33 | format: zip 34 | 35 | checksum: 36 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 37 | 38 | changelog: 39 | use: git 40 | format: "{{.SHA}}: {{.Message}} (@{{.AuthorUsername}})" 41 | sort: asc 42 | filters: 43 | exclude: 44 | - '^docs(\(.*\))?:' # 排除以 "docs:" 或 "docs(readme):" 开头的提交 45 | - '^chore(\(.*\))?:' # 排除以 "chore:" 或 "chore(ci):" 开头的提交 46 | - '^test(\(.*\))?:' # 排除以 "test:" 或 "test(unit):" 开头的提交 47 | groups: 48 | - title: "New Features" 49 | regexp: '^feat(\(.*\))?:' # 匹配 "feat:" 或 "feat(flow):" 50 | - title: "Bug Fixes" 51 | regexp: '^fix(\(.*\))?:' # 匹配 "fix:" 或 "fix(bug):" 52 | - title: "Performance Improvements" 53 | regexp: '^perf(\(.*\))?:' # 匹配 "perf:" 或 "perf(optimize):" 54 | - title: "Code Refactoring" 55 | regexp: '^refactor(\(.*\))?:' 56 | - title: "Other Changes" 57 | regexp: '^.*$' # 匹配所有其他提交 58 | 59 | release: 60 | draft: false 61 | prerelease: false 62 | mode: replace 63 | 64 | git: 65 | ignore_tags: 66 | - alpha 67 | - beta -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS builder 2 | RUN set -eux && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 3 | 4 | ARG APP_RELATIVE_PATH 5 | 6 | COPY . /data/app 7 | WORKDIR /data/app 8 | 9 | RUN rm -rf /data/app/bin/ 10 | #RUN export GOPROXY=https://goproxy.cn,direct && go mod tidy && go build -ldflags="-s -w" -o ./bin/server ${APP_RELATIVE_PATH} 11 | RUN go mod tidy && go build -ldflags="-s -w" -o ./bin/server ${APP_RELATIVE_PATH} 12 | RUN mv config /data/app/bin/ 13 | 14 | 15 | FROM alpine:3.14 16 | RUN set -eux && sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories 17 | 18 | 19 | RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 20 | && echo "Asia/Shanghai" > /etc/timezone \ 21 | && apk del tzdata 22 | 23 | 24 | ARG APP_ENV 25 | ENV APP_ENV=${APP_ENV} 26 | 27 | WORKDIR /data/app 28 | COPY --from=builder /data/app/bin /data/app 29 | COPY --from=builder /data/app/web/upload /data/app/web/upload/ 30 | RUN mkdir -p /data/app/storage/ 31 | 32 | EXPOSE 8000 33 | ENTRYPOINT [ "./server" ] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 陈通 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: init 2 | init: 3 | go install github.com/google/wire/cmd/wire@latest 4 | go install github.com/swaggo/swag/cmd/swag@latest 5 | go install github.com/incu6us/goimports-reviser/v3@latest 6 | go install mvdan.cc/gofumpt@latest 7 | 8 | .PHONY: build 9 | build: 10 | go build -ldflags="-s -w" -o ./bin/server ./cmd/server 11 | 12 | .PHONY: docker 13 | docker: 14 | docker build -t webstack-go:v2 --build-arg APP_CONF=config/prod.yml --build-arg APP_RELATIVE_PATH=./cmd/server . 15 | docker run -itd -p 8000:8000 --name webstack-go webstack-go:v2 16 | 17 | .PHONY: swag 18 | swag: 19 | swag init -g cmd/server/main.go -o ./docs --parseDependency 20 | 21 | .PHONY: fmt 22 | fmt: 23 | goimports-reviser -rm-unused -set-alias -format ./... 24 | find . -name '*.go' -not -name "*.pb.go" -not -name "*.gen.go" | xargs gofumpt -w -extra 25 | 26 | .PHONY: run 27 | run: 28 | go mod tidy 29 | go build -ldflags="-s -w" -o ./bin/server ./cmd/server 30 | ./bin/server -conf=config/prod.yml 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webstack-go 网址导航后台系统 2 | 3 | 基于 Golang 开源的网址导航网站项目,具备完整的前后台,您可以拿来制作自己平日收藏的网址导航。 4 | > v1: 使用 mysql 和 redis 组件, 丰富的后端功能。 v2: 简化版无需额外组件, 使用轻量级 sqlite 数据库。 5 | 6 | - 前端模板: [WebStackPage](https://github.com/WebStackPage/WebStackPage.github.io)、[光年后台模板](https://gitee.com/yinqi/Light-Year-Admin-Using-Iframe-v4) 7 | - 后端框架: 基于 [go-nunu](https://github.com/go-nunu/nunu) 脚手架搭建 8 | 9 | 功能: 10 | - [x] 新增 webstack - 导航首页 11 | - [x] 新增 仪表盘 (SSE) 12 | - [x] 新增 网站管理 - 网站分类 13 | - [x] 新增 网站管理 - 网站列表 14 | - [x] 新增 系统管理 - 自定义导航基本信息 (Logo、favicon、备案信息等) 15 | - [x] 新增 支持批量添加 (自动获取标题、Logo、网站描述) 16 | - [x] 新增 一键同步、导出功能 17 | - [x] 新增 由 [gorm-gen](https://github.com/go-gorm/gen) 代码生成提供支持的友好且更安全的 GORM 18 | - [x] 杂项 与仓库保持同步 [Docker Hub](https://hub.docker.com/r/ch3nnn/webstack-go/tags) 19 | 20 | ## 快速开始 21 | 22 | ### 一、运行环境 23 | 24 | - Golang 1.22 25 | - SQLite 26 | 27 | ### 二、启动服务 28 | 29 | **1、二进制文件** 30 | 31 | 你可以直接从[ Releases ](https://github.com/ch3nnn/webstack-go/releases)下载预先编译好的二进制文件,解压后执行: 32 | 33 | ```bash 34 | ./webstack-go -conf config/prod.yml 35 | ``` 36 | 37 | > [!NOTE] 38 | > MacOS 在执行二进制文件时会提示:未打开“webstack-go”,因为 Apple 无法检查其是否包含恶意软件。 39 | > 40 | > 可在“系统设置 > 隐私与安全性 > 安全性”中点击“仍然允许”,然后再次尝试执行二进制文件。 41 | 42 | 43 | **2、源码运行服务 (需要 Golang 环境)** 44 | 1. 目录下执行 `go mod tidy` 拉取项目依赖库 45 | 2. 执行 `go build -o ./bin/server ./cmd/server` 编译项目,生成可执行文件 server 46 | 3. 编译完执行 `./bin/server -conf=config/prod.yml` 首次启动程序之后,会生成 SQLite 数据库,并自动创建表结构 47 | 48 | 49 | **3、Docker 运行服务** 50 | #### 下载镜像 51 | 1. docker run 运行 52 | ```bash 53 | docker run -i -t --restart always -p 8000:8000 --name webstack-go -v ./data/storage:/data/app/storage ch3nnn/webstack-go:latest 54 | ``` 55 | 56 | 2. docker compose (推荐) 57 | ```yaml 58 | services: 59 | webstack-go: 60 | stdin_open: true 61 | tty: true 62 | restart: always 63 | ports: 64 | - 8000:8000 65 | container_name: webstack-go 66 | image: ch3nnn/webstack-go:latest 67 | volumes: 68 | - ./data/storage:/data/app/storage 69 | ``` 70 | 71 | #### 本地编译 72 | 1. 目录下执行 `make docker` 等待启动 73 | ```shell 74 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 75 | 5cb641ff3950 webstack-go:v2 "./server" 5 seconds ago Up 5 seconds 0.0.0.0:8000->8000/tcp webstack-go 76 | ``` 77 | 2. docker container 正常运行后, 在浏览器中打开界面,链接地址:http://127.0.0.1:8000 78 | 79 | ## 效果图 80 | 81 | > **首页** 82 | 83 | ![](.github/image/index.png) 84 | 85 | > **网站分类** 86 | 87 | ![](.github/image/category.png) 88 | 89 | > **新增网站** 90 | 91 | ![](.github/image/add_site.png) 92 | 93 | > **网站信息** 94 | 95 | ![](.github/image/site.png) 96 | 97 | > **网站配置** 98 | 99 | ![](.github/image/config.png) 100 | 101 | ## Star History 102 | 103 | [![Star History Chart](https://api.star-history.com/svg?repos=ch3nnn/webstack-go&type=Date)](https://star-history.com/#ch3nnn/webstack-go&Date) 104 | -------------------------------------------------------------------------------- /api/v1/category.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 上午10:24 4 | */ 5 | 6 | package v1 7 | 8 | import "time" 9 | 10 | type Category struct { 11 | ID int `json:"id"` // 主键ID 12 | ParentID int `json:"parent_id"` // 父级分类ID 13 | Sort int `json:"sort"` // 排序 14 | Title string `json:"title"` // 名称 15 | Icon string `json:"icon"` // 图标 16 | CreatedAt *time.Time `json:"created_at"` // 创建时间 17 | UpdatedAt *time.Time `json:"updated_at"` // 更新时间 18 | IsUsed bool `json:"is_used"` // 是否启用 1:是 0:否 19 | Level int32 `json:"level"` // 分类等级 20 | } 21 | 22 | type ( 23 | CategoryCreateReq struct { 24 | ParentID int `form:"parent_id"` // 分类父ID 25 | Level int32 `form:"level"` // 分类等级 1 一级分类 2 二级分类 26 | Name string `form:"name"` // 菜单名称 27 | Icon string `form:"icon"` // 图标 28 | IsUsed bool `form:"is_used"` // 是否启用 1:是 0:否 29 | SortID int `form:"sort_id"` // 排序 ID 30 | } 31 | 32 | CategoryCreateResp struct { 33 | Category // 分类信息 34 | } 35 | ) 36 | 37 | type ( 38 | CategoryList struct { 39 | Id int `json:"id"` // ID 40 | Pid int `json:"pid"` // 父类ID 41 | Name string `json:"name"` // 菜单名称 42 | Link string `json:"link"` // 链接地址 43 | Icon string `json:"icon"` // 图标 44 | IsUsed bool `json:"is_used"` // 是否启用 1=启用 0=禁用 45 | Sort int `json:"sort"` // 排序 46 | Level int32 `json:"level"` // 分类等级 1 一级分类 2 二级分类 47 | } 48 | 49 | CategoryListReq struct{} 50 | 51 | CategoryListResp struct { 52 | List []CategoryList `json:"list"` // 分类列表 53 | } 54 | ) 55 | 56 | type ( 57 | CategoryDeleteReq struct { 58 | ID int `uri:"id" binding:"required"` // ID 59 | } 60 | 61 | CategoryDeleteResp struct{} 62 | ) 63 | 64 | type ( 65 | CategoryDetailReq struct { 66 | ID int `uri:"id" binding:"required"` // ID 67 | } 68 | 69 | CategoryDetailResp struct { 70 | Id int `json:"id"` // 主键ID 71 | Pid int `json:"pid"` // 父类ID 72 | Name string `json:"name"` // 分类名称 73 | Icon string `json:"icon"` // 图标 74 | IsAdd bool `json:"is_add"` // 是否新增子分类 75 | SortID int `json:"sort_id"` // 排序 ID 76 | } 77 | ) 78 | 79 | type ( 80 | CategoryUpdateReq struct { 81 | ID int `form:"id" binding:"required"` // ID 82 | Pid *int `form:"parent_id"` // 父类ID 83 | Name *string `form:"name"` // 菜单名称 84 | Icon *string `form:"icon"` // 图标 85 | IsUsed *bool `form:"used"` // 是否启用 86 | SortID *int `form:"sort_id"` // 排序 ID 87 | } 88 | 89 | CategoryUpdateResp struct { 90 | Category // 分类信息 91 | } 92 | ) 93 | -------------------------------------------------------------------------------- /api/v1/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/17 19:36 4 | */ 5 | 6 | package v1 7 | 8 | import "mime/multipart" 9 | 10 | type ConfigResp struct { 11 | ID int `json:"id"` // 主键ID 12 | AboutSite string `json:"about_site"` // 关于网站的信息 13 | AboutAuthor string `json:"about_author"` // 关于作者的信息 14 | IsAbout bool `json:"is_about"` // 是否显示关于信息 15 | SiteTitle string `json:"site_title"` // 网站标题 16 | SiteKeyword string `json:"site_keyword"` // 网站关键词 17 | SiteDesc string `json:"site_desc"` // 网站描述 18 | SiteRecord string `json:"site_record"` // 网站备案号 19 | SiteURL string `json:"site_url"` // 网站备案管理 url 20 | SiteLogo string `json:"site_logo"` // 网站Logo URL 21 | SiteFavicon string `json:"site_favicon"` // 网站Favicon URL 22 | } 23 | 24 | type ( 25 | ConfigUpdateReq struct { 26 | AboutSite *string `json:"about_site" form:"about_site"` // 关于网站的信息 27 | AboutAuthor *string `json:"about_author" form:"about_author"` // 关于作者的信息 28 | IsAbout *bool `json:"is_about" form:"is_about"` // 是否显示关于信息 29 | SiteTitle *string `json:"site_title" form:"site_title"` // 网站标题 30 | SiteKeyword *string `json:"site_keyword" form:"site_keyword"` // 网站关键词 31 | SiteDesc *string `json:"site_desc" form:"site_desc"` // 网站描述 32 | SiteRecord *string `json:"site_record" form:"site_record"` // 网站备案号 33 | SiteURL *string `json:"site_url" form:"site_url"` // 网站备案管理 url 34 | LogoFile *multipart.FileHeader `json:"logo" form:"logo"` // 上传 logo 图片 35 | FaviconFile *multipart.FileHeader `json:"favicon" form:"favicon"` // 上传 favicon 图片 36 | } 37 | 38 | ConfigUpdateResp struct { 39 | ID int `json:"id"` // 主键ID 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /api/v1/dashboard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/11/12 12:32 4 | */ 5 | 6 | package v1 7 | 8 | type DashboardResp struct { 9 | ProjectVersion string // 项目版本 10 | GoOS string // 操作系统 11 | GoArch string // 架构 12 | GoVersion string // go版本 13 | ProjectPath string // 项目路径 14 | Host string // 主机名 15 | Env string // 环境 16 | MemTotal string // 内存 17 | MemUsed string // 已用内存 18 | MemUsedPercent float64 // 已用内存百分比 19 | DiskTotal string // 磁盘 20 | DiskUsed string // 已用磁盘 21 | DiskUsedPercent float64 // 已用磁盘百分比 22 | CpuName string // cpu名称 23 | CpuCores int32 // cpu核数 24 | CpuUsedPercent float64 // cpu使用率 25 | } 26 | -------------------------------------------------------------------------------- /api/v1/errors.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | var ( 4 | ErrSuccess = newError(0, "ok") 5 | ErrBadRequest = newError(400, "Bad Request") 6 | ErrUnauthorized = newError(401, "Unauthorized") 7 | ErrNotFound = newError(404, "Not Found") 8 | ErrInternalServerError = newError(500, "Internal Server Error") 9 | ) 10 | 11 | var ( 12 | ErrorUserNameAndPassword = newError(100, "用户名和密码错误") 13 | ErrorUserOldPassword = newError(100, "原密码错误") 14 | ErrorTokenGeneration = newError(101, "令牌生成错误") 15 | 16 | ErrorUnableToGetFile = newError(200, "无法获取文件") 17 | ErrorFileSizeExceedsLimit = newError(201, "文件大小超过限制") 18 | ) 19 | -------------------------------------------------------------------------------- /api/v1/index.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午1:50 4 | */ 5 | 6 | package v1 7 | 8 | import "github.com/ch3nnn/webstack-go/internal/dal/model" 9 | 10 | type TreeNode struct { 11 | Id int // 节点ID 12 | Pid int // 父节点ID 13 | Name string // 节点名称 14 | Icon string // 图标 15 | Sort int // 排序 16 | Child []*TreeNode // 获取子节点切片 17 | } 18 | 19 | type CategorySite struct { 20 | Category string // 分类 21 | SiteList []model.StSite // 站点列表 22 | } 23 | 24 | type About struct { 25 | AboutSite string `json:"about_site"` // 关于站点 26 | AboutAuthor string `json:"about_author"` // 关于作者 27 | IsAbout bool `json:"is_about"` // 是否开启关于 28 | } 29 | 30 | type ConfigSite struct { 31 | SiteTitle string `json:"site_title"` // 站点标题 32 | SiteKeyword string `json:"site_keyword"` // 站点关键字 33 | SiteDesc string `json:"site_desc"` // 站点描述 34 | SiteRecord string `json:"site_record"` // 站点备案 35 | SiteURL string `json:"site_url"` // 备案url 36 | SiteLogo string `json:"site_logo"` // 站点logo 37 | SiteFavicon string `json:"site_favicon"` // 站点favicon 38 | } 39 | 40 | type IndexResp struct { 41 | About *About // 关于页面 42 | ConfigSite *ConfigSite // 站点配置 43 | CategoryTree []*TreeNode // 分类树 44 | CategorySites []*CategorySite // 归类站点数据 45 | } 46 | 47 | type AboutResp struct { 48 | About 49 | } 50 | -------------------------------------------------------------------------------- /api/v1/user.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | type ( 4 | LoginReq struct { 5 | Username string `form:"username" json:"username" binding:"required" example:"admin"` // 用户名 6 | Password string `form:"password,default=value" json:"password,default=123456" example:"123456"` // 密码 7 | } 8 | 9 | LoginResp struct { 10 | Token string `json:"token"` // JWT 11 | } 12 | ) 13 | 14 | type ( 15 | UpdatePasswordReq struct { 16 | OldPassword string `form:"old_password"` // 旧密码 17 | NewPassword string `form:"new_password"` // 新密码 18 | } 19 | 20 | UpdatePasswordResp struct{} 21 | ) 22 | 23 | type ( 24 | Menu struct { 25 | Id int `json:"id"` // ID 26 | Pid int `json:"pid"` // 父类ID 27 | Name string `json:"name"` // 菜单名称 28 | Link string `json:"link"` // 链接地址 29 | Icon string `json:"icon"` // 图标 30 | } 31 | 32 | InfoReq struct{} 33 | 34 | InfoResp struct { 35 | Username string `json:"username"` // 用户名 36 | Menus []Menu `json:"menu"` // 菜单栏 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /api/v1/v1.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | type Response struct { 12 | Code int `json:"code"` 13 | Message string `json:"message"` 14 | Data interface{} `json:"data"` 15 | } 16 | 17 | func SSEStream(ctx *gin.Context, dataChan chan interface{}, eventName string) { 18 | if eventName == "" { 19 | eventName = "message" 20 | } 21 | 22 | // 设置响应头 23 | ctx.Header("Content-Type", "text/event-stream") 24 | ctx.Header("Cache-Control", "no-cache") 25 | ctx.Header("Connection", "keep-alive") 26 | 27 | // 使用 ctx.Stream 发送数据 28 | ctx.Stream(func(w io.Writer) bool { 29 | if data, ok := <-dataChan; ok { 30 | // 将数据作为 SSE 事件发送 31 | ctx.SSEvent(eventName, data) 32 | return true // 继续流 33 | } 34 | return false // 关闭流 35 | }) 36 | } 37 | 38 | func HandleSuccess(ctx *gin.Context, data interface{}) { 39 | if data == nil { 40 | data = map[string]interface{}{} 41 | } 42 | 43 | ctx.JSON(http.StatusOK, data) 44 | } 45 | 46 | func HandleError(ctx *gin.Context, httpCode int, err error, data interface{}) { 47 | if data == nil { 48 | data = map[string]string{} 49 | } 50 | resp := Response{Code: errorCodeMap[err], Message: err.Error(), Data: data} 51 | if _, ok := errorCodeMap[ErrSuccess]; !ok { 52 | resp = Response{Code: 500, Message: "unknown error", Data: data} 53 | } 54 | ctx.JSON(httpCode, resp) 55 | } 56 | 57 | func ErrHandler404(c *gin.Context) { 58 | c.HTML(http.StatusOK, "404.html", gin.H{"title": "404 Error - Page not found"}) 59 | } 60 | 61 | type Error struct { 62 | Code int 63 | Message string 64 | } 65 | 66 | var errorCodeMap = map[error]int{} 67 | 68 | func newError(code int, msg string) error { 69 | err := errors.New(msg) 70 | errorCodeMap[err] = code 71 | return err 72 | } 73 | 74 | func (e Error) Error() string { 75 | return e.Message 76 | } 77 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "go.uber.org/zap" 9 | 10 | "github.com/ch3nnn/webstack-go/cmd/server/wire" 11 | "github.com/ch3nnn/webstack-go/pkg/config" 12 | "github.com/ch3nnn/webstack-go/pkg/log" 13 | ) 14 | 15 | // @title webstack-go API 16 | // @version 1.0.0 17 | // @description This is a sample server celler server. 18 | // @termsOfService http://swagger.io/terms/ 19 | // @contact.name API Support 20 | // @contact.url http://www.swagger.io/support 21 | // @contact.email support@swagger.io 22 | // @license.name Apache 2.0 23 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 24 | // @host localhost:8000 25 | // @securityDefinitions.apiKey Bearer 26 | // @in header 27 | // @name Authorization 28 | // @externalDocs.description OpenAPI 29 | // @externalDocs.url https://swagger.io/resources/open-api/ 30 | func main() { 31 | envConf := flag.String("conf", "config/local.yml", "config path, eg: -conf ./config/local.yml") 32 | flag.Parse() 33 | 34 | conf := config.NewConfig(*envConf) 35 | logger := log.NewLog(conf) 36 | 37 | app, cleanup, err := wire.NewWire(conf, logger) 38 | defer cleanup() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | logger.Info(fmt.Sprintf("load conf file: %s", *envConf)) 44 | logger.Info("server start", zap.String("host", fmt.Sprintf("http://%s:%d", conf.GetString("http.host"), conf.GetInt("http.port")))) 45 | logger.Info("docs addr", zap.String("addr", fmt.Sprintf("http://%s:%d/swagger/index.html", conf.GetString("http.host"), conf.GetInt("http.port")))) 46 | 47 | if err = app.Run(context.Background()); err != nil { 48 | panic(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cmd/server/wire/wire.go: -------------------------------------------------------------------------------- 1 | //go:build wireinject 2 | // +build wireinject 3 | 4 | package wire 5 | 6 | import ( 7 | "github.com/google/wire" 8 | "github.com/spf13/viper" 9 | 10 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 11 | "github.com/ch3nnn/webstack-go/internal/handler" 12 | categoryHandler "github.com/ch3nnn/webstack-go/internal/handler/category" 13 | configHandler "github.com/ch3nnn/webstack-go/internal/handler/config" 14 | dashboardHandler "github.com/ch3nnn/webstack-go/internal/handler/dashboard" 15 | indexHandler "github.com/ch3nnn/webstack-go/internal/handler/index" 16 | siteHandler "github.com/ch3nnn/webstack-go/internal/handler/site" 17 | userHandler "github.com/ch3nnn/webstack-go/internal/handler/user" 18 | "github.com/ch3nnn/webstack-go/internal/server" 19 | "github.com/ch3nnn/webstack-go/internal/service" 20 | categoryService "github.com/ch3nnn/webstack-go/internal/service/category" 21 | configService "github.com/ch3nnn/webstack-go/internal/service/config" 22 | dashboardService "github.com/ch3nnn/webstack-go/internal/service/dashboard" 23 | indexService "github.com/ch3nnn/webstack-go/internal/service/index" 24 | siteService "github.com/ch3nnn/webstack-go/internal/service/site" 25 | userService "github.com/ch3nnn/webstack-go/internal/service/user" 26 | "github.com/ch3nnn/webstack-go/pkg/app" 27 | "github.com/ch3nnn/webstack-go/pkg/jwt" 28 | "github.com/ch3nnn/webstack-go/pkg/log" 29 | "github.com/ch3nnn/webstack-go/pkg/server/http" 30 | ) 31 | 32 | var repositorySet = wire.NewSet( 33 | repository.NewDB, 34 | repository.NewRepository, 35 | repository.NewSysUserDao, 36 | repository.NewStCategoryDao, 37 | repository.NewStSiteDao, 38 | repository.NewSysUserMenuDao, 39 | repository.NewSysConfigDao, 40 | repository.NewSysMenuDao, 41 | ) 42 | 43 | var handlerSet = wire.NewSet( 44 | handler.NewHandler, 45 | userHandler.NewHandler, 46 | indexHandler.NewHandler, 47 | siteHandler.NewHandler, 48 | categoryHandler.NewHandler, 49 | dashboardHandler.NewHandler, 50 | configHandler.NewHandler, 51 | ) 52 | 53 | var serviceSet = wire.NewSet( 54 | service.NewService, 55 | userService.NewService, 56 | indexService.NewService, 57 | siteService.NewService, 58 | categoryService.NewService, 59 | configService.NewService, 60 | dashboardService.NewService, 61 | ) 62 | 63 | var serverSet = wire.NewSet( 64 | server.NewHTTPServer, 65 | ) 66 | 67 | // build App 68 | func newApp(httpServer *http.Server) *app.App { 69 | return app.NewApp( 70 | app.WithServer(httpServer), 71 | app.WithName("webstack-go"), 72 | ) 73 | } 74 | 75 | func NewWire(*viper.Viper, *log.Logger) (*app.App, func(), error) { 76 | panic(wire.Build( 77 | serverSet, 78 | serviceSet, 79 | handlerSet, 80 | repositorySet, 81 | jwt.NewJwt, 82 | http.NewGinDefaultServer, 83 | newApp, 84 | )) 85 | } 86 | -------------------------------------------------------------------------------- /config/local.yml: -------------------------------------------------------------------------------- 1 | env: local 2 | http: 3 | host: 0.0.0.0 4 | port: 8000 5 | security: 6 | jwt: 7 | key: QQYnRFerJTSEcrfB89fw8prOaObmrch8 8 | data: 9 | db: 10 | user: 11 | driver: sqlite 12 | dsn: storage/webstack-go.db?_busy_timeout=5000 13 | 14 | log: 15 | log_level: debug 16 | encoding: console # json or console 17 | log_file_name: "./storage/logs/server.log" 18 | max_backups: 30 19 | max_age: 7 20 | max_size: 1024 21 | compress: true 22 | -------------------------------------------------------------------------------- /config/prod.yml: -------------------------------------------------------------------------------- 1 | env: prod 2 | http: 3 | host: 0.0.0.0 4 | port: 8000 5 | security: 6 | jwt: 7 | key: QQYnRFerJTSEcrfB89fw8prOaObmrch8 8 | data: 9 | db: 10 | user: 11 | driver: sqlite 12 | dsn: storage/webstack-go.db?_busy_timeout=5000 13 | 14 | log: 15 | log_level: info 16 | encoding: json # json or console 17 | log_file_name: "./storage/logs/server.log" 18 | max_backups: 30 19 | max_age: 7 20 | max_size: 1024 21 | compress: true 22 | -------------------------------------------------------------------------------- /config/test.yml: -------------------------------------------------------------------------------- 1 | env: local 2 | http: 3 | host: 0.0.0.0 4 | port: 8000 5 | security: 6 | jwt: 7 | key: test 8 | data: 9 | db: 10 | user: 11 | driver: sqlite 12 | dsn: storage/webstack-go.db?_busy_timeout=5000 13 | 14 | log: 15 | log_level: error 16 | encoding: console # json or console 17 | log_file_name: "./storage/logs/server.log" 18 | max_backups: 30 19 | max_age: 7 20 | max_size: 1024 21 | compress: true 22 | -------------------------------------------------------------------------------- /internal/dal/model/st_category.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNameStCategory = "st_category" 12 | 13 | // StCategory mapped from table 14 | type StCategory struct { 15 | ID int `gorm:"column:id;type:INTEGER;primaryKey" json:"id"` 16 | ParentID int `gorm:"column:parent_id;type:int(11);not null" json:"parent_id"` 17 | Sort int `gorm:"column:sort;type:int(11);not null" json:"sort"` 18 | Title string `gorm:"column:title;type:varchar(50);not null" json:"title"` 19 | Icon string `gorm:"column:icon;type:varchar(20);not null" json:"icon"` 20 | Level int32 `gorm:"column:level;type:integer;not null" json:"level"` 21 | IsUsed bool `gorm:"column:is_used;type:bool;default:false" json:"is_used"` 22 | CreatedAt *time.Time `gorm:"column:created_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"created_at"` 23 | UpdatedAt *time.Time `gorm:"column:updated_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"updated_at"` 24 | DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime" json:"deleted_at"` 25 | } 26 | 27 | // TableName StCategory's table name 28 | func (*StCategory) TableName() string { 29 | return TableNameStCategory 30 | } 31 | -------------------------------------------------------------------------------- /internal/dal/model/st_site.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNameStSite = "st_site" 12 | 13 | // StSite mapped from table 14 | type StSite struct { 15 | ID int `gorm:"column:id;type:INTEGER;primaryKey" json:"id"` 16 | CategoryID int `gorm:"column:category_id;type:int(11)" json:"category_id"` 17 | Title string `gorm:"column:title;type:varchar(50)" json:"title"` 18 | Icon string `gorm:"column:icon;type:text" json:"icon"` 19 | Description string `gorm:"column:description;type:varchar(500)" json:"description"` 20 | URL string `gorm:"column:url;type:varchar(255);not null" json:"url"` 21 | IsUsed bool `gorm:"column:is_used;type:bool;default:false" json:"is_used"` 22 | CreatedAt *time.Time `gorm:"column:created_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"created_at"` 23 | UpdatedAt *time.Time `gorm:"column:updated_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"updated_at"` 24 | DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime" json:"deleted_at"` 25 | Sort int `gorm:"column:sort;type:int(11)" json:"sort"` 26 | } 27 | 28 | // TableName StSite's table name 29 | func (*StSite) TableName() string { 30 | return TableNameStSite 31 | } 32 | -------------------------------------------------------------------------------- /internal/dal/model/sys_config.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | const TableNameSysConfig = "sys_config" 8 | 9 | // SysConfig mapped from table 10 | type SysConfig struct { 11 | ID int `gorm:"column:id;type:INTEGER;primaryKey" json:"id"` 12 | AboutSite string `gorm:"column:about_site;type:text" json:"about_site"` 13 | AboutAuthor string `gorm:"column:about_author;type:text" json:"about_author"` 14 | IsAbout bool `gorm:"column:is_about;type:bool;default:false" json:"is_about"` 15 | SiteTitle string `gorm:"column:site_title;type:varchar(50)" json:"site_title"` 16 | SiteKeyword string `gorm:"column:site_keyword;type:text" json:"site_keyword"` 17 | SiteDesc string `gorm:"column:site_desc;type:text" json:"site_desc"` 18 | SiteRecord string `gorm:"column:site_record;type:varchar(500)" json:"site_record"` 19 | SiteLogo string `gorm:"column:site_logo;type:text" json:"site_logo"` 20 | SiteFavicon string `gorm:"column:site_favicon;type:text" json:"site_favicon"` 21 | SiteURL string `gorm:"column:site_url;type:varchar(500)" json:"site_url"` 22 | } 23 | 24 | // TableName SysConfig's table name 25 | func (*SysConfig) TableName() string { 26 | return TableNameSysConfig 27 | } 28 | -------------------------------------------------------------------------------- /internal/dal/model/sys_menu.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNameSysMenu = "sys_menu" 12 | 13 | // SysMenu mapped from table 14 | type SysMenu struct { 15 | ID int `gorm:"column:id;type:INTEGER;primaryKey" json:"id"` 16 | Pid int `gorm:"column:pid;type:int(11);not null" json:"pid"` 17 | Name string `gorm:"column:name;type:varchar(32);not null" json:"name"` 18 | Link string `gorm:"column:link;type:varchar(100);not null" json:"link"` 19 | Icon string `gorm:"column:icon;type:varchar(60);not null" json:"icon"` 20 | Level int `gorm:"column:level;type:int(11);default:1" json:"level"` 21 | Sort int `gorm:"column:sort;type:int(11);not null" json:"sort"` 22 | IsUsed bool `gorm:"column:is_used;type:bool;default:false" json:"is_used"` 23 | CreatedAt *time.Time `gorm:"column:created_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"created_at"` 24 | UpdatedAt *time.Time `gorm:"column:updated_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"updated_at"` 25 | DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime" json:"deleted_at"` 26 | } 27 | 28 | // TableName SysMenu's table name 29 | func (*SysMenu) TableName() string { 30 | return TableNameSysMenu 31 | } 32 | -------------------------------------------------------------------------------- /internal/dal/model/sys_user.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNameSysUser = "sys_user" 12 | 13 | // SysUser mapped from table 14 | type SysUser struct { 15 | ID int `gorm:"column:id;type:INTEGER;primaryKey" json:"id"` 16 | Username string `gorm:"column:username;type:varchar(32);not null" json:"username"` 17 | Password string `gorm:"column:password;type:varchar(100);not null" json:"password"` 18 | CreatedAt *time.Time `gorm:"column:created_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"created_at"` 19 | UpdatedAt *time.Time `gorm:"column:updated_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"updated_at"` 20 | DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime" json:"deleted_at"` 21 | } 22 | 23 | // TableName SysUser's table name 24 | func (*SysUser) TableName() string { 25 | return TableNameSysUser 26 | } 27 | -------------------------------------------------------------------------------- /internal/dal/model/sys_user_menu.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | const TableNameSysUserMenu = "sys_user_menu" 12 | 13 | // SysUserMenu mapped from table 14 | type SysUserMenu struct { 15 | ID int `gorm:"column:id;type:INTEGER;primaryKey" json:"id"` 16 | UserID int `gorm:"column:user_id;type:int(11);not null" json:"user_id"` 17 | MenuID int `gorm:"column:menu_id;type:int(11);not null" json:"menu_id"` 18 | CreatedAt *time.Time `gorm:"column:created_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"created_at"` 19 | UpdatedAt *time.Time `gorm:"column:updated_at;type:datetime;not null;default:CURRENT_TIMESTAMP not null" json:"updated_at"` 20 | DeletedAt *time.Time `gorm:"column:deleted_at;type:datetime" json:"deleted_at"` 21 | } 22 | 23 | // TableName SysUserMenu's table name 24 | func (*SysUserMenu) TableName() string { 25 | return TableNameSysUserMenu 26 | } 27 | -------------------------------------------------------------------------------- /internal/dal/repository/st_category.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/gen" 7 | "gorm.io/gen/field" 8 | 9 | "github.com/ch3nnn/webstack-go/internal/dal/model" 10 | "github.com/ch3nnn/webstack-go/internal/dal/query" 11 | ) 12 | 13 | var _ iCustomGenStCategoryFunc = (*customStCategoryDao)(nil) 14 | 15 | type ( 16 | // IStCategoryDao not edit interface name 17 | IStCategoryDao interface { 18 | iWhereStCategoryFunc 19 | WithContext(ctx context.Context) iCustomGenStCategoryFunc 20 | 21 | // TODO Custom WhereFunc .... 22 | // ... 23 | } 24 | 25 | // not edit interface name 26 | iCustomGenStCategoryFunc interface { 27 | iGenStCategoryFunc 28 | 29 | // TODO Custom DaoFunc .... 30 | // ... 31 | 32 | FindAllOrderBySort(orderColumn field.Expr, whereFunc ...func(dao gen.Dao) gen.Dao) ([]*model.StCategory, error) 33 | } 34 | 35 | // not edit interface name 36 | customStCategoryDao struct { 37 | stCategoryDao 38 | } 39 | ) 40 | 41 | func NewStCategoryDao() IStCategoryDao { 42 | return &customStCategoryDao{ 43 | stCategoryDao{ 44 | stCategoryDo: query.StCategory.WithContext(context.Background()), 45 | }, 46 | } 47 | } 48 | 49 | func (d *customStCategoryDao) WithContext(ctx context.Context) iCustomGenStCategoryFunc { 50 | d.stCategoryDo = d.stCategoryDo.WithContext(ctx) 51 | return d 52 | } 53 | 54 | func (d *customStCategoryDao) FindAllOrderBySort(orderColumn field.Expr, whereFunc ...func(dao gen.Dao) gen.Dao) ([]*model.StCategory, error) { 55 | return d.stCategoryDo.Scopes(whereFunc...).Order(orderColumn).Find() 56 | } 57 | -------------------------------------------------------------------------------- /internal/dal/repository/st_site.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/gen" 7 | "gorm.io/gen/field" 8 | 9 | "github.com/ch3nnn/webstack-go/internal/dal/model" 10 | "github.com/ch3nnn/webstack-go/internal/dal/query" 11 | "github.com/ch3nnn/webstack-go/pkg/gormx" 12 | ) 13 | 14 | var _ iCustomGenStSiteFunc = (*customStSiteDao)(nil) 15 | 16 | type ( 17 | // IStSiteDao not edit interface name 18 | IStSiteDao interface { 19 | iWhereStSiteFunc 20 | WithContext(ctx context.Context) iCustomGenStSiteFunc 21 | 22 | // TODO Custom WhereFunc .... 23 | // ... 24 | LikeInByTitleOrDescOrURL(search string) func(dao gen.Dao) gen.Dao 25 | } 26 | 27 | // not edit interface name 28 | iCustomGenStSiteFunc interface { 29 | iGenStSiteFunc 30 | 31 | // TODO Custom DaoFunc .... 32 | // ... 33 | 34 | FindSiteCategoryWithPage(page, pageSize int, result any, orderColumns []field.Expr, whereFunc ...func(dao gen.Dao) gen.Dao) (count int64, err error) 35 | } 36 | 37 | // not edit interface name 38 | customStSiteDao struct { 39 | stSiteDao 40 | } 41 | ) 42 | 43 | type ( 44 | SiteCategory struct { 45 | model.StSite 46 | model.StCategory 47 | } 48 | ) 49 | 50 | func NewStSiteDao() IStSiteDao { 51 | return &customStSiteDao{ 52 | stSiteDao{ 53 | stSiteDo: query.StSite.WithContext(context.Background()), 54 | }, 55 | } 56 | } 57 | 58 | func (d *customStSiteDao) WithContext(ctx context.Context) iCustomGenStSiteFunc { 59 | d.stSiteDo = d.stSiteDo.WithContext(ctx) 60 | return d 61 | } 62 | 63 | func (d *customStSiteDao) LikeInByTitleOrDescOrURL(search string) func(dao gen.Dao) gen.Dao { 64 | return func(dao gen.Dao) gen.Dao { 65 | return dao.Where( 66 | d.stSiteDo. 67 | Where( 68 | query.StSite.Title.Like(gormx.LikeInner(search)), 69 | ). 70 | Or( 71 | query.StSite.Description.Like(gormx.LikeInner(search)), 72 | ). 73 | Or( 74 | query.StSite.URL.Like(gormx.LikeInner(search)), 75 | ), 76 | ) 77 | } 78 | } 79 | 80 | func (d *customStSiteDao) FindSiteCategoryWithPage(page, pageSize int, result any, orderColumns []field.Expr, whereFunc ...func(dao gen.Dao) gen.Dao) (count int64, err error) { 81 | return d.stSiteDo. 82 | Select( 83 | field.NewAsterisk(query.StSite.TableName()), 84 | field.NewAsterisk(query.StCategory.TableName()), 85 | ). 86 | LeftJoin( 87 | query.StCategory, 88 | query.StCategory.ID.EqCol(query.StSite.CategoryID), 89 | ). 90 | Order( 91 | orderColumns..., 92 | ). 93 | Scopes(whereFunc...). 94 | ScanByPage(result, (page-1)*pageSize, pageSize) 95 | } 96 | -------------------------------------------------------------------------------- /internal/dal/repository/sys_menu.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ch3nnn/webstack-go/internal/dal/model" 7 | "github.com/ch3nnn/webstack-go/internal/dal/query" 8 | ) 9 | 10 | var DefaultSysMenuAdmin = []*model.SysMenu{ 11 | { 12 | ID: 1, 13 | Pid: 0, 14 | Name: "网站管理", 15 | Icon: "users", 16 | Level: 1, 17 | Sort: 500, 18 | IsUsed: true, 19 | }, 20 | { 21 | ID: 2, 22 | Pid: 1, 23 | Name: "网站分类", 24 | Link: "/admin/category", 25 | Level: 1, 26 | Sort: 501, 27 | IsUsed: true, 28 | }, 29 | { 30 | ID: 3, 31 | Pid: 1, 32 | Name: "网站信息", 33 | Link: "/admin/site", 34 | Level: 1, 35 | Sort: 502, 36 | IsUsed: true, 37 | }, 38 | { 39 | ID: 4, 40 | Pid: 0, 41 | Name: "系统管理", 42 | Level: 1, 43 | Sort: 600, 44 | IsUsed: true, 45 | }, 46 | { 47 | ID: 5, 48 | Pid: 4, 49 | Name: "网站配置", 50 | Link: "/admin/config", 51 | Level: 1, 52 | Sort: 601, 53 | IsUsed: true, 54 | }, 55 | } 56 | 57 | var _ iCustomGenSysMenuFunc = (*customSysMenuDao)(nil) 58 | 59 | type ( 60 | // ISysMenuDao not edit interface name 61 | ISysMenuDao interface { 62 | iWhereSysMenuFunc 63 | WithContext(ctx context.Context) iCustomGenSysMenuFunc 64 | 65 | // TODO Custom WhereFunc .... 66 | // ... 67 | } 68 | 69 | // not edit interface name 70 | iCustomGenSysMenuFunc interface { 71 | iGenSysMenuFunc 72 | 73 | // TODO Custom DaoFunc .... 74 | // ... 75 | } 76 | 77 | // not edit interface name 78 | customSysMenuDao struct { 79 | sysMenuDao 80 | } 81 | ) 82 | 83 | func NewSysMenuDao() ISysMenuDao { 84 | return &customSysMenuDao{ 85 | sysMenuDao{ 86 | sysMenuDo: query.SysMenu.WithContext(context.Background()), 87 | }, 88 | } 89 | } 90 | 91 | func (d *customSysMenuDao) WithContext(ctx context.Context) iCustomGenSysMenuFunc { 92 | d.sysMenuDo = d.sysMenuDo.WithContext(ctx) 93 | return d 94 | } 95 | -------------------------------------------------------------------------------- /internal/dal/repository/sys_user.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ch3nnn/webstack-go/internal/dal/query" 7 | ) 8 | 9 | const ( 10 | DefaultUname = "admin" 11 | DefaultUPassword = "admin" 12 | ) 13 | 14 | var _ iCustomGenSysUserFunc = (*customSysUserDao)(nil) 15 | 16 | type ( 17 | // ISysUserDao not edit interface name 18 | ISysUserDao interface { 19 | iWhereSysUserFunc 20 | WithContext(ctx context.Context) iCustomGenSysUserFunc 21 | 22 | // TODO Custom WhereFunc .... 23 | // ... 24 | } 25 | 26 | // not edit interface name 27 | iCustomGenSysUserFunc interface { 28 | iGenSysUserFunc 29 | 30 | // TODO Custom DaoFunc .... 31 | // ... 32 | } 33 | 34 | // not edit interface name 35 | customSysUserDao struct { 36 | sysUserDao 37 | } 38 | ) 39 | 40 | func NewSysUserDao() ISysUserDao { 41 | return &customSysUserDao{ 42 | sysUserDao{ 43 | sysUserDo: query.SysUser.WithContext(context.Background()), 44 | }, 45 | } 46 | } 47 | 48 | func (d *customSysUserDao) WithContext(ctx context.Context) iCustomGenSysUserFunc { 49 | d.sysUserDo = d.sysUserDo.WithContext(ctx) 50 | return d 51 | } 52 | -------------------------------------------------------------------------------- /internal/dal/repository/sys_user_menu.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ch3nnn/webstack-go/internal/dal/model" 7 | "github.com/ch3nnn/webstack-go/internal/dal/query" 8 | ) 9 | 10 | // DefaultSysUserMenuAdmin is used to store the menu information of the admin user DefaultSysMenuAdmin 11 | var DefaultSysUserMenuAdmin = []*model.SysUserMenu{ 12 | { 13 | UserID: 1, 14 | MenuID: 1, 15 | }, 16 | { 17 | UserID: 1, 18 | MenuID: 2, 19 | }, 20 | { 21 | UserID: 1, 22 | MenuID: 3, 23 | }, 24 | { 25 | UserID: 1, 26 | MenuID: 4, 27 | }, 28 | { 29 | UserID: 1, 30 | MenuID: 5, 31 | }, 32 | } 33 | 34 | var _ iCustomGenSysUserMenuFunc = (*customSysUserMenuDao)(nil) 35 | 36 | type ( 37 | // ISysUserMenuDao not edit interface name 38 | ISysUserMenuDao interface { 39 | iWhereSysUserMenuFunc 40 | WithContext(ctx context.Context) iCustomGenSysUserMenuFunc 41 | 42 | // TODO Custom WhereFunc .... 43 | // ... 44 | } 45 | 46 | // not edit interface name 47 | iCustomGenSysUserMenuFunc interface { 48 | iGenSysUserMenuFunc 49 | 50 | // TODO Custom DaoFunc .... 51 | // ... 52 | } 53 | 54 | // not edit interface name 55 | customSysUserMenuDao struct { 56 | sysUserMenuDao 57 | } 58 | ) 59 | 60 | func NewSysUserMenuDao() ISysUserMenuDao { 61 | return &customSysUserMenuDao{ 62 | sysUserMenuDao{ 63 | sysUserMenuDo: query.SysUserMenu.WithContext(context.Background()), 64 | }, 65 | } 66 | } 67 | 68 | func (d *customSysUserMenuDao) WithContext(ctx context.Context) iCustomGenSysUserMenuFunc { 69 | d.sysUserMenuDo = d.sysUserMenuDo.WithContext(ctx) 70 | return d 71 | } 72 | -------------------------------------------------------------------------------- /internal/handler/category/create.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/27 下午11:55 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | // Create 17 | // Register godoc 18 | // @Summary 新增分类 19 | // @Schemes 20 | // @Description 新增分类 21 | // @Tags 分类模块 22 | // @Accept json 23 | // @Produce json 24 | // @Param request body v1.CategoryCreateReq true "params" 25 | // @Success 200 {object} v1.CategoryCreateResp 26 | // @Router /api/admin/category [post] 27 | func (h *Handler) Create(ctx *gin.Context) { 28 | var req v1.CategoryCreateReq 29 | if err := ctx.ShouldBind(&req); err != nil { 30 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 31 | return 32 | } 33 | 34 | resp, err := h.categoryService.Create(ctx, &req) 35 | if err != nil { 36 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 37 | return 38 | } 39 | 40 | v1.HandleSuccess(ctx, resp) 41 | } 42 | -------------------------------------------------------------------------------- /internal/handler/category/delete.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/27 下午11:30 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | // Delete 17 | // Register godoc 18 | // @Summary 删除分类 19 | // @Schemes 20 | // @Description 删除分类 21 | // @Tags 分类模块 22 | // @Accept json 23 | // @Produce json 24 | // @Param request body v1.CategoryDeleteReq true "params" 25 | // @Success 200 {object} v1.CategoryDeleteResp 26 | // @Router /api/admin/category/:id [delete] 27 | func (h *Handler) Delete(ctx *gin.Context) { 28 | var req v1.CategoryDeleteReq 29 | if err := ctx.ShouldBindUri(&req); err != nil { 30 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 31 | return 32 | } 33 | 34 | if _, err := h.categoryService.Delete(ctx, &req); err != nil { 35 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 36 | return 37 | } 38 | 39 | v1.HandleSuccess(ctx, nil) 40 | } 41 | -------------------------------------------------------------------------------- /internal/handler/category/detail.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/13 下午11:55 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | // Detail 17 | // Register godoc 18 | // @Summary 详情分类 19 | // @Schemes 20 | // @Description 详情分类 21 | // @Tags 分类模块 22 | // @Accept json 23 | // @Produce json 24 | // @Param request body v1.CategoryDetailReq true "params" 25 | // @Success 200 {object} v1.CategoryDetailResp 26 | // @Router /api/admin/category/:id [get] 27 | func (h *Handler) Detail(ctx *gin.Context) { 28 | var req v1.CategoryDetailReq 29 | if err := ctx.ShouldBindUri(&req); err != nil { 30 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 31 | return 32 | } 33 | 34 | resp, err := h.categoryService.Detail(ctx, &req) 35 | if err != nil { 36 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 37 | return 38 | } 39 | 40 | v1.HandleSuccess(ctx, resp) 41 | } 42 | -------------------------------------------------------------------------------- /internal/handler/category/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午1:46 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "github.com/ch3nnn/webstack-go/internal/handler" 10 | "github.com/ch3nnn/webstack-go/internal/service/category" 11 | ) 12 | 13 | type Handler struct { 14 | *handler.Handler 15 | categoryService category.Service 16 | } 17 | 18 | func NewHandler(handler *handler.Handler, categoryService category.Service) *Handler { 19 | return &Handler{ 20 | Handler: handler, 21 | categoryService: categoryService, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/handler/category/list.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/11 下午11:49 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | // List 17 | // Register godoc 18 | // @Summary 列表分类 19 | // @Schemes 20 | // @Description 列表分类 21 | // @Tags 分类模块 22 | // @Accept json 23 | // @Produce json 24 | // @Success 200 {object} v1.CategoryListResp 25 | // @Router /api/admin/category [get] 26 | func (h *Handler) List(ctx *gin.Context) { 27 | resp, err := h.categoryService.List(ctx, nil) 28 | if err != nil { 29 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 30 | return 31 | } 32 | 33 | v1.HandleSuccess(ctx, resp) 34 | } 35 | -------------------------------------------------------------------------------- /internal/handler/category/update.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/13 下午11:13 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | // Update 17 | // Register godoc 18 | // @Summary 更新分类 19 | // @Schemes 20 | // @Description 更新分类 21 | // @Tags 分类模块 22 | // @Accept json 23 | // @Produce json 24 | // @Param request body v1.CategoryUpdateReq true "params" 25 | // @Success 200 {object} v1.CategoryUpdateResp 26 | // @Router /api/admin/category/update [put] 27 | func (h *Handler) Update(ctx *gin.Context) { 28 | var req v1.CategoryUpdateReq 29 | if err := ctx.ShouldBind(&req); err != nil { 30 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 31 | return 32 | } 33 | 34 | resp, err := h.categoryService.Update(ctx, &req) 35 | if err != nil { 36 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 37 | return 38 | } 39 | 40 | v1.HandleSuccess(ctx, resp) 41 | } 42 | -------------------------------------------------------------------------------- /internal/handler/config/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/17 21:21 4 | */ 5 | 6 | package config 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Config(ctx *gin.Context) { 17 | resp, err := h.configService.GetConfig(ctx) 18 | if err != nil { 19 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 20 | return 21 | } 22 | 23 | v1.HandleSuccess(ctx, resp) 24 | } 25 | -------------------------------------------------------------------------------- /internal/handler/config/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/17 下午7:32 4 | */ 5 | 6 | package config 7 | 8 | import ( 9 | "github.com/ch3nnn/webstack-go/internal/handler" 10 | "github.com/ch3nnn/webstack-go/internal/service/config" 11 | ) 12 | 13 | type Handler struct { 14 | *handler.Handler 15 | configService config.Service 16 | } 17 | 18 | func NewHandler(handler *handler.Handler, configService config.Service) *Handler { 19 | return &Handler{ 20 | Handler: handler, 21 | configService: configService, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/handler/config/update.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/18 14:21 4 | */ 5 | 6 | package config 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Update(ctx *gin.Context) { 17 | var req v1.ConfigUpdateReq 18 | if err := ctx.ShouldBind(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.configService.Update(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/handler/dashboard/dashboard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/07 19:48 4 | */ 5 | 6 | package dashboard 7 | 8 | import ( 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "go.uber.org/zap" 13 | 14 | v1 "github.com/ch3nnn/webstack-go/api/v1" 15 | ) 16 | 17 | func (h *Handler) Dashboard(ctx *gin.Context) { 18 | messageChan := make(chan any) 19 | go func() { 20 | for { 21 | dashboard, err := h.dashboardService.Dashboard(ctx) 22 | if err != nil { 23 | h.Logger.Error("SSE(Server-Sent Events)dashboard api", zap.Error(err)) 24 | return 25 | } 26 | 27 | select { 28 | case messageChan <- dashboard: 29 | default: 30 | return 31 | } 32 | 33 | time.Sleep(1 * time.Second) 34 | } 35 | }() 36 | 37 | v1.SSEStream(ctx, messageChan, "dashboard") 38 | } 39 | -------------------------------------------------------------------------------- /internal/handler/dashboard/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午1:46 4 | */ 5 | 6 | package dashboard 7 | 8 | import ( 9 | "github.com/ch3nnn/webstack-go/internal/handler" 10 | "github.com/ch3nnn/webstack-go/internal/service/dashboard" 11 | ) 12 | 13 | type Handler struct { 14 | *handler.Handler 15 | dashboardService dashboard.Service 16 | } 17 | 18 | func NewHandler(handler *handler.Handler, dashboardService dashboard.Service) *Handler { 19 | return &Handler{Handler: handler, dashboardService: dashboardService} 20 | } 21 | -------------------------------------------------------------------------------- /internal/handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/ch3nnn/webstack-go/pkg/log" 5 | ) 6 | 7 | type Handler struct { 8 | Logger *log.Logger 9 | } 10 | 11 | func NewHandler(logger *log.Logger) *Handler { 12 | return &Handler{ 13 | Logger: logger, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/handler/handler_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/08 22:01 4 | */ 5 | 6 | package handler 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | "testing" 14 | 15 | "github.com/ch3nnn/webstack-go/pkg/config" 16 | "github.com/ch3nnn/webstack-go/pkg/log" 17 | ) 18 | 19 | func Test_Handler(t *testing.T) { 20 | err := os.Setenv("APP_CONF", "../../config/test.yml") 21 | if err != nil { 22 | fmt.Println("Setenv error", err) 23 | } 24 | envConf := flag.String("conf", "config/test.yml", "config path, eg: -conf ./config/test.yml") 25 | flag.Parse() 26 | conf := config.NewConfig(*envConf) 27 | 28 | logPath := filepath.Join("../../../", conf.GetString("log.log_file_name")) 29 | conf.Set("log.log_file_name", logPath) 30 | 31 | NewHandler(log.NewLog(conf)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/handler/index/about.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/18 20:45 4 | */ 5 | 6 | package index 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) About(ctx *gin.Context) { 17 | resp, err := h.indexService.About(ctx) 18 | if err != nil { 19 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 20 | return 21 | } 22 | 23 | v1.HandleSuccess(ctx, resp) 24 | } 25 | -------------------------------------------------------------------------------- /internal/handler/index/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午1:46 4 | */ 5 | 6 | package index 7 | 8 | import ( 9 | "github.com/ch3nnn/webstack-go/internal/handler" 10 | "github.com/ch3nnn/webstack-go/internal/service/index" 11 | ) 12 | 13 | type Handler struct { 14 | *handler.Handler 15 | indexService index.Service 16 | } 17 | 18 | func NewHandler(handler *handler.Handler, indexService index.Service) *Handler { 19 | return &Handler{Handler: handler, indexService: indexService} 20 | } 21 | -------------------------------------------------------------------------------- /internal/handler/index/index.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午1:46 4 | */ 5 | 6 | package index 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Index(ctx *gin.Context) { 17 | resp, err := h.indexService.Index(ctx) 18 | if err != nil { 19 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 20 | return 21 | } 22 | 23 | ctx.HTML(http.StatusOK, "index.html", resp) 24 | } 25 | -------------------------------------------------------------------------------- /internal/handler/site/create.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/04 下午5:55 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Create(ctx *gin.Context) { 17 | var req v1.SiteCreateReq 18 | if err := ctx.ShouldBind(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.siteService.BatchCreate(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/handler/site/delete.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/28 上午12:25 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Delete(ctx *gin.Context) { 17 | var req v1.SiteDeleteReq 18 | if err := ctx.ShouldBindUri(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.siteService.Delete(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/handler/site/export.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/29 19:06 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Export(ctx *gin.Context) { 17 | var rep v1.SiteExportReq 18 | if err := ctx.BindQuery(&rep); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.siteService.Export(ctx, &rep) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | // 设置响应头 Excel 文件 30 | ctx.Header("Content-Type", "application/octet-stream") 31 | ctx.Header("Content-Disposition", "attachment; filename=sites.xlsx") 32 | ctx.Header("Content-Transfer-Encoding", "binary") 33 | 34 | if err = resp.File.Write(ctx.Writer); err != nil { 35 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 36 | return 37 | } 38 | 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /internal/handler/site/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午12:35 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "github.com/ch3nnn/webstack-go/internal/handler" 10 | "github.com/ch3nnn/webstack-go/internal/service/site" 11 | ) 12 | 13 | type Handler struct { 14 | *handler.Handler 15 | siteService site.Service 16 | } 17 | 18 | func NewHandler(handler *handler.Handler, siteService site.Service) *Handler { 19 | return &Handler{ 20 | Handler: handler, 21 | siteService: siteService, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/handler/site/list.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 下午11:48 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) List(ctx *gin.Context) { 17 | var req v1.SiteListReq 18 | if err := ctx.ShouldBind(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.siteService.List(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/handler/site/sync.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/11/12 16:37 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) SyncSite(ctx *gin.Context) { 17 | var req v1.SiteSyncReq 18 | if err := ctx.ShouldBindUri(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.siteService.Sync(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/handler/site/update.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/30 下午10:07 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Update(ctx *gin.Context) { 17 | var req v1.SiteUpdateReq 18 | if err := ctx.ShouldBindUri(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | if err := ctx.ShouldBind(&req); err != nil { 24 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 25 | return 26 | } 27 | 28 | resp, err := h.siteService.Update(ctx, &req) 29 | if err != nil { 30 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 31 | return 32 | } 33 | 34 | v1.HandleSuccess(ctx, resp) 35 | } 36 | -------------------------------------------------------------------------------- /internal/handler/user/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午12:35 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "github.com/ch3nnn/webstack-go/internal/handler" 10 | "github.com/ch3nnn/webstack-go/internal/service/user" 11 | ) 12 | 13 | type Handler struct { 14 | *handler.Handler 15 | userService user.Service 16 | } 17 | 18 | func NewHandler(handler *handler.Handler, userService user.Service) *Handler { 19 | return &Handler{ 20 | Handler: handler, 21 | userService: userService, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/handler/user/info.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 下午4:00 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Info(ctx *gin.Context) { 17 | resp, err := h.userService.Info(ctx, nil) 18 | if err != nil { 19 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 20 | return 21 | } 22 | 23 | v1.HandleSuccess(ctx, resp) 24 | } 25 | -------------------------------------------------------------------------------- /internal/handler/user/login.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午12:36 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) Login(ctx *gin.Context) { 17 | var req v1.LoginReq 18 | if err := ctx.Bind(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.userService.Login(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/handler/user/logout.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/11/12 13:26 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | ) 13 | 14 | func (h *Handler) Logout(ctx *gin.Context) { 15 | v1.HandleSuccess(ctx, nil) 16 | } 17 | -------------------------------------------------------------------------------- /internal/handler/user/updatepassword.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/11/11 18:38 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "net/http" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | ) 15 | 16 | func (h *Handler) UpdatePassword(ctx *gin.Context) { 17 | var req v1.UpdatePasswordReq 18 | if err := ctx.ShouldBind(&req); err != nil { 19 | v1.HandleError(ctx, http.StatusBadRequest, v1.ErrBadRequest, nil) 20 | return 21 | } 22 | 23 | resp, err := h.userService.UpdatePassword(ctx, &req) 24 | if err != nil { 25 | v1.HandleError(ctx, http.StatusInternalServerError, err, nil) 26 | return 27 | } 28 | 29 | v1.HandleSuccess(ctx, resp) 30 | } 31 | -------------------------------------------------------------------------------- /internal/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func CORSMiddleware() gin.HandlerFunc { 10 | return func(c *gin.Context) { 11 | method := c.Request.Method 12 | c.Header("Access-Control-Allow-Origin", c.GetHeader("Origin")) 13 | c.Header("Access-Control-Allow-Credentials", "true") 14 | 15 | if method == "OPTIONS" { 16 | c.Header("Access-Control-Allow-Methods", c.GetHeader("Access-Control-Request-Method")) 17 | c.Header("Access-Control-Allow-Headers", c.GetHeader("Access-Control-Request-Headers")) 18 | c.Header("Access-Control-Max-Age", "7200") 19 | c.AbortWithStatus(http.StatusNoContent) 20 | return 21 | } 22 | c.Next() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | "github.com/ch3nnn/webstack-go/api/v1" 10 | "github.com/ch3nnn/webstack-go/pkg/jwt" 11 | "github.com/ch3nnn/webstack-go/pkg/log" 12 | ) 13 | 14 | const ( 15 | UserID string = "uid" 16 | Claims string = "claims" 17 | ) 18 | 19 | func StrictAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc { 20 | return func(ctx *gin.Context) { 21 | tokenString := ctx.Request.Header.Get("Token") 22 | if tokenString == "" { 23 | tokenString, _ = ctx.Cookie("_login_token_") 24 | } 25 | if tokenString == "" { 26 | tokenString = ctx.Query("Token") 27 | if tokenString == "" { 28 | tokenString = ctx.Query("token") 29 | } 30 | } 31 | if tokenString == "" { 32 | logger.WithContext(ctx).Warn("No token", zap.Any("data", map[string]interface{}{ 33 | "url": ctx.Request.URL, 34 | "params": ctx.Params, 35 | })) 36 | v1.HandleError(ctx, http.StatusUnauthorized, v1.ErrUnauthorized, nil) 37 | ctx.Abort() 38 | return 39 | } 40 | 41 | claims, err := j.ParseToken(tokenString) 42 | if err != nil { 43 | logger.WithContext(ctx).Error("token error", zap.Any("data", map[string]interface{}{ 44 | "url": ctx.Request.URL, 45 | "params": ctx.Params, 46 | }), zap.Error(err)) 47 | v1.HandleError(ctx, http.StatusUnauthorized, v1.ErrUnauthorized, nil) 48 | ctx.Abort() 49 | return 50 | } 51 | 52 | ctx.Set(UserID, claims.UserID) 53 | ctx.Set(Claims, claims) 54 | 55 | recoveryLoggerFunc(ctx, logger) 56 | ctx.Next() 57 | } 58 | } 59 | 60 | func NoStrictAuth(j *jwt.JWT, logger *log.Logger) gin.HandlerFunc { 61 | return func(ctx *gin.Context) { 62 | tokenString := ctx.Request.Header.Get("Token") 63 | if tokenString == "" { 64 | tokenString, _ = ctx.Cookie("_login_token_") 65 | } 66 | if tokenString == "" { 67 | tokenString = ctx.Query("Token") 68 | if tokenString == "" { 69 | tokenString = ctx.Query("token") 70 | } 71 | } 72 | if tokenString == "" { 73 | logger.WithContext(ctx).Warn("No token", zap.Any("data", map[string]interface{}{ 74 | "url": ctx.Request.URL, 75 | "params": ctx.Params, 76 | })) 77 | ctx.Redirect(http.StatusFound, "/login") 78 | ctx.Abort() 79 | return 80 | } 81 | 82 | claims, err := j.ParseToken(tokenString) 83 | if err != nil { 84 | logger.WithContext(ctx).Error("token error", zap.Any("data", map[string]interface{}{ 85 | "url": ctx.Request.URL, 86 | "params": ctx.Params, 87 | }), zap.Error(err)) 88 | 89 | ctx.Redirect(http.StatusFound, "/login") 90 | ctx.Abort() 91 | return 92 | } 93 | 94 | ctx.Set(UserID, claims.UserID) 95 | ctx.Set(Claims, claims) 96 | 97 | recoveryLoggerFunc(ctx, logger) 98 | ctx.Next() 99 | } 100 | } 101 | 102 | func recoveryLoggerFunc(ctx *gin.Context, logger *log.Logger) { 103 | if userInfo, ok := ctx.MustGet("claims").(*jwt.MyCustomClaims); ok { 104 | logger.WithValue(ctx, zap.Int("UserID", userInfo.UserID)) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /internal/middleware/log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "time" 7 | 8 | "github.com/duke-git/lancet/v2/cryptor" 9 | "github.com/duke-git/lancet/v2/random" 10 | "github.com/gin-gonic/gin" 11 | "go.uber.org/zap" 12 | 13 | "github.com/ch3nnn/webstack-go/pkg/log" 14 | ) 15 | 16 | func RequestLogMiddleware(logger *log.Logger) gin.HandlerFunc { 17 | return func(ctx *gin.Context) { 18 | // The configuration is initialized once per request 19 | uuid, err := random.UUIdV4() 20 | if err != nil { 21 | return 22 | } 23 | 24 | trace := cryptor.Md5String(uuid) 25 | logger.WithValue(ctx, zap.String("trace", trace)) 26 | logger.WithValue(ctx, zap.String("request_method", ctx.Request.Method)) 27 | logger.WithValue(ctx, zap.Any("request_headers", ctx.Request.Header)) 28 | logger.WithValue(ctx, zap.String("request_url", ctx.Request.URL.String())) 29 | 30 | if ctx.Request.Body != nil { 31 | bodyBytes, _ := ctx.GetRawData() 32 | ctx.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) // 关键点 33 | logger.WithValue(ctx, zap.String("request_params", string(bodyBytes))) 34 | } 35 | 36 | logger.WithContext(ctx).Info("Request") 37 | ctx.Next() 38 | } 39 | } 40 | 41 | func ResponseLogMiddleware(logger *log.Logger) gin.HandlerFunc { 42 | return func(ctx *gin.Context) { 43 | blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: ctx.Writer} 44 | ctx.Writer = blw 45 | startTime := time.Now() 46 | ctx.Next() 47 | duration := time.Since(startTime).String() 48 | logger.WithContext(ctx).Info("Response", zap.Any("response_body", blw.body.String()), zap.Any("time", duration)) 49 | } 50 | } 51 | 52 | type bodyLogWriter struct { 53 | gin.ResponseWriter 54 | body *bytes.Buffer 55 | } 56 | 57 | func (w bodyLogWriter) Write(b []byte) (int, error) { 58 | w.body.Write(b) 59 | return w.ResponseWriter.Write(b) 60 | } 61 | -------------------------------------------------------------------------------- /internal/service/category/create.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 上午11:14 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | "github.com/ch3nnn/webstack-go/internal/dal/model" 13 | ) 14 | 15 | func (s *service) Create(ctx context.Context, req *v1.CategoryCreateReq) (*v1.CategoryCreateResp, error) { 16 | category, err := s.categoryRepo.WithContext(ctx). 17 | Create(&model.StCategory{ 18 | ParentID: req.ParentID, 19 | Title: req.Name, 20 | Icon: req.Icon, 21 | Level: req.Level, 22 | IsUsed: req.IsUsed, 23 | Sort: req.SortID, 24 | }) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | return &v1.CategoryCreateResp{Category: v1.Category{ 30 | ID: category.ID, 31 | ParentID: category.ParentID, 32 | Sort: category.Sort, 33 | Title: category.Title, 34 | Icon: category.Icon, 35 | CreatedAt: category.CreatedAt, 36 | UpdatedAt: category.UpdatedAt, 37 | IsUsed: category.IsUsed, 38 | Level: category.Level, 39 | }}, nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/service/category/delete.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 下午5:48 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | ) 13 | 14 | func (s *service) Delete(ctx context.Context, req *v1.CategoryDeleteReq) (*v1.CategoryDeleteResp, error) { 15 | return nil, s.categoryRepo.WithContext(ctx).Delete(s.categoryRepo.WhereByID(req.ID)) 16 | } 17 | -------------------------------------------------------------------------------- /internal/service/category/detail.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 上午11:03 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | ) 13 | 14 | func (s *service) Detail(ctx context.Context, req *v1.CategoryDetailReq) (*v1.CategoryDetailResp, error) { 15 | category, err := s.categoryRepo.WithContext(ctx).FindOne(s.categoryRepo.WhereByID(req.ID)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &v1.CategoryDetailResp{ 21 | Id: category.ID, 22 | Pid: category.ParentID, 23 | Name: category.Title, 24 | Icon: category.Icon, 25 | IsAdd: category.ParentID == 0, 26 | SortID: category.Sort, 27 | }, err 28 | } 29 | -------------------------------------------------------------------------------- /internal/service/category/list.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 上午10:23 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | "github.com/ch3nnn/webstack-go/internal/dal/query" 13 | ) 14 | 15 | func (s *service) List(ctx context.Context, _ *v1.CategoryListReq) (*v1.CategoryListResp, error) { 16 | categories, err := s.categoryRepo.WithContext(ctx).FindAllOrderBySort(query.StCategory.Sort.Abs()) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | categoryList := make([]v1.CategoryList, len(categories)) 22 | for i, category := range categories { 23 | categoryList[i] = v1.CategoryList{ 24 | Id: category.ID, 25 | Pid: category.ParentID, 26 | Name: category.Title, 27 | Icon: category.Icon, 28 | IsUsed: category.IsUsed, 29 | Sort: category.Sort, 30 | Level: category.Level, 31 | } 32 | } 33 | 34 | return &v1.CategoryListResp{List: categoryList}, err 35 | } 36 | -------------------------------------------------------------------------------- /internal/service/category/service.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 上午10:23 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 13 | s "github.com/ch3nnn/webstack-go/internal/service" 14 | ) 15 | 16 | var _ Service = (*service)(nil) 17 | 18 | type Service interface { 19 | i() 20 | 21 | // Update 更新分类 22 | Update(ctx context.Context, req *v1.CategoryUpdateReq) (resp *v1.CategoryUpdateResp, err error) 23 | // Detail 获取分类详情 24 | Detail(ctx context.Context, req *v1.CategoryDetailReq) (resp *v1.CategoryDetailResp, err error) 25 | // List 获取分类列表 26 | List(ctx context.Context, req *v1.CategoryListReq) (resp *v1.CategoryListResp, err error) 27 | // Create 创建分类 28 | Create(ctx context.Context, req *v1.CategoryCreateReq) (resp *v1.CategoryCreateResp, err error) 29 | // Delete 删除分类 30 | Delete(ctx context.Context, req *v1.CategoryDeleteReq) (resp *v1.CategoryDeleteResp, err error) 31 | } 32 | 33 | type service struct { 34 | *s.Service 35 | categoryRepo repository.IStCategoryDao 36 | } 37 | 38 | func NewService( 39 | s *s.Service, 40 | categoryRepo repository.IStCategoryDao, 41 | ) Service { 42 | return &service{ 43 | Service: s, 44 | categoryRepo: categoryRepo, 45 | } 46 | } 47 | 48 | func (s *service) i() {} 49 | -------------------------------------------------------------------------------- /internal/service/category/update.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/13 下午11:17 4 | */ 5 | 6 | package category 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | "github.com/ch3nnn/webstack-go/internal/dal/query" 13 | "github.com/ch3nnn/webstack-go/pkg/gormx" 14 | ) 15 | 16 | func (s *service) Update(ctx context.Context, req *v1.CategoryUpdateReq) (*v1.CategoryUpdateResp, error) { 17 | update := make(map[string]any) 18 | 19 | if req.Pid != nil { 20 | column := gormx.ColumnName(query.StCategory.ParentID) 21 | update[column] = req.Pid 22 | } 23 | if req.Icon != nil { 24 | column := gormx.ColumnName(query.StCategory.Icon) 25 | update[column] = req.Icon 26 | } 27 | if req.Name != nil { 28 | column := gormx.ColumnName(query.StCategory.Title) 29 | update[column] = req.Name 30 | } 31 | if req.SortID != nil { 32 | column := gormx.ColumnName(query.StCategory.Sort) 33 | update[column] = req.SortID 34 | } 35 | if req.IsUsed != nil { 36 | column := gormx.ColumnName(query.StCategory.IsUsed) 37 | update[column] = req.IsUsed 38 | } 39 | 40 | _, err := s.categoryRepo.WithContext(ctx).Update(update, s.categoryRepo.WhereByID(req.ID)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | category, err := s.categoryRepo.WithContext(ctx).FindOne(s.categoryRepo.WhereByID(req.ID)) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &v1.CategoryUpdateResp{ 51 | Category: v1.Category{ 52 | ID: category.ID, 53 | ParentID: category.ParentID, 54 | Sort: category.Sort, 55 | Title: category.Title, 56 | Icon: category.Icon, 57 | CreatedAt: category.CreatedAt, 58 | UpdatedAt: category.UpdatedAt, 59 | IsUsed: category.IsUsed, 60 | Level: category.Level, 61 | }, 62 | }, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/service/config/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/18 21:10 4 | */ 5 | 6 | package config 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | ) 13 | 14 | func (s *service) GetConfig(ctx context.Context) (*v1.ConfigResp, error) { 15 | conf, err := s.configRepo.WithContext(ctx).FindOne() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &v1.ConfigResp{ 21 | ID: conf.ID, 22 | AboutSite: conf.AboutSite, 23 | AboutAuthor: conf.AboutAuthor, 24 | IsAbout: conf.IsAbout, 25 | SiteTitle: conf.SiteTitle, 26 | SiteKeyword: conf.SiteKeyword, 27 | SiteDesc: conf.SiteDesc, 28 | SiteRecord: conf.SiteRecord, 29 | SiteURL: conf.SiteURL, 30 | SiteLogo: conf.SiteLogo, 31 | SiteFavicon: conf.SiteFavicon, 32 | }, nil 33 | } 34 | -------------------------------------------------------------------------------- /internal/service/config/service.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/17 下午7:32 4 | */ 5 | 6 | package config 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 15 | s "github.com/ch3nnn/webstack-go/internal/service" 16 | ) 17 | 18 | var _ Service = (*service)(nil) 19 | 20 | type Service interface { 21 | i() 22 | 23 | // GetConfig 获取配置信息 24 | GetConfig(ctx context.Context) (*v1.ConfigResp, error) 25 | // Update 更新配置信息 26 | Update(ctx *gin.Context, req *v1.ConfigUpdateReq) (resp *v1.ConfigUpdateResp, err error) 27 | } 28 | 29 | type service struct { 30 | *s.Service 31 | configRepo repository.ISysConfigDao 32 | } 33 | 34 | func NewService( 35 | s *s.Service, 36 | configRepo repository.ISysConfigDao, 37 | ) Service { 38 | return &service{ 39 | Service: s, 40 | configRepo: configRepo, 41 | } 42 | } 43 | 44 | func (s *service) i() {} 45 | -------------------------------------------------------------------------------- /internal/service/config/service.mockgen.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/service/config/service.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=internal/service/config/service.go -destination internal/service/config/service.mockgen.go -package=config 7 | // 8 | 9 | // Package config is a generated GoMock package. 10 | package config 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | v1 "github.com/ch3nnn/webstack-go/api/v1" 17 | gin "github.com/gin-gonic/gin" 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockService is a mock of Service interface. 22 | type MockService struct { 23 | ctrl *gomock.Controller 24 | recorder *MockServiceMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockServiceMockRecorder is the mock recorder for MockService. 29 | type MockServiceMockRecorder struct { 30 | mock *MockService 31 | } 32 | 33 | // NewMockService creates a new mock instance. 34 | func NewMockService(ctrl *gomock.Controller) *MockService { 35 | mock := &MockService{ctrl: ctrl} 36 | mock.recorder = &MockServiceMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockService) EXPECT() *MockServiceMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // GetConfig mocks base method. 46 | func (m *MockService) GetConfig(ctx context.Context) (*v1.ConfigResp, error) { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "GetConfig", ctx) 49 | ret0, _ := ret[0].(*v1.ConfigResp) 50 | ret1, _ := ret[1].(error) 51 | return ret0, ret1 52 | } 53 | 54 | // GetConfig indicates an expected call of GetConfig. 55 | func (mr *MockServiceMockRecorder) GetConfig(ctx any) *gomock.Call { 56 | mr.mock.ctrl.T.Helper() 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockService)(nil).GetConfig), ctx) 58 | } 59 | 60 | // Update mocks base method. 61 | func (m *MockService) Update(ctx *gin.Context, req *v1.ConfigUpdateReq) (*v1.ConfigUpdateResp, error) { 62 | m.ctrl.T.Helper() 63 | ret := m.ctrl.Call(m, "Update", ctx, req) 64 | ret0, _ := ret[0].(*v1.ConfigUpdateResp) 65 | ret1, _ := ret[1].(error) 66 | return ret0, ret1 67 | } 68 | 69 | // Update indicates an expected call of Update. 70 | func (mr *MockServiceMockRecorder) Update(ctx, req any) *gomock.Call { 71 | mr.mock.ctrl.T.Helper() 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockService)(nil).Update), ctx, req) 73 | } 74 | 75 | // i mocks base method. 76 | func (m *MockService) i() { 77 | m.ctrl.T.Helper() 78 | m.ctrl.Call(m, "i") 79 | } 80 | 81 | // i indicates an expected call of i. 82 | func (mr *MockServiceMockRecorder) i() *gomock.Call { 83 | mr.mock.ctrl.T.Helper() 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "i", reflect.TypeOf((*MockService)(nil).i)) 85 | } 86 | -------------------------------------------------------------------------------- /internal/service/config/update.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/18 14:21 4 | */ 5 | 6 | package config 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | "github.com/ch3nnn/webstack-go/internal/dal/query" 13 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 14 | "github.com/ch3nnn/webstack-go/pkg/gormx" 15 | "github.com/ch3nnn/webstack-go/pkg/tools" 16 | ) 17 | 18 | const ( 19 | LogoWidth = 200 20 | LogoHeight = 50 21 | FaviconWidth = 64 22 | FaviconHeight = 64 23 | ) 24 | 25 | func (s *service) Update(ctx *gin.Context, req *v1.ConfigUpdateReq) (resp *v1.ConfigUpdateResp, err error) { 26 | update := make(map[string]any) 27 | if req.SiteTitle != nil { 28 | column := gormx.ColumnName(query.SysConfig.SiteTitle) 29 | update[column] = *req.SiteTitle 30 | } 31 | if req.SiteDesc != nil { 32 | column := gormx.ColumnName(query.SysConfig.SiteDesc) 33 | update[column] = *req.SiteDesc 34 | } 35 | if req.SiteKeyword != nil { 36 | column := gormx.ColumnName(query.SysConfig.SiteKeyword) 37 | update[column] = *req.SiteKeyword 38 | } 39 | if req.SiteRecord != nil { 40 | column := gormx.ColumnName(query.SysConfig.SiteRecord) 41 | update[column] = *req.SiteRecord 42 | } 43 | if req.SiteURL != nil { 44 | column := gormx.ColumnName(query.SysConfig.SiteURL) 45 | update[column] = *req.SiteURL 46 | } 47 | if req.AboutSite != nil { 48 | column := gormx.ColumnName(query.SysConfig.AboutSite) 49 | update[column] = *req.AboutSite 50 | } 51 | if req.AboutAuthor != nil { 52 | column := gormx.ColumnName(query.SysConfig.AboutAuthor) 53 | update[column] = *req.AboutAuthor 54 | } 55 | if req.IsAbout != nil { 56 | column := gormx.ColumnName(query.SysConfig.IsAbout) 57 | update[column] = *req.IsAbout 58 | } 59 | if req.LogoFile != nil && req.LogoFile.Size > 0 { 60 | base64Str, err := tools.ResizeMultipartImgToBase64(req.LogoFile, LogoWidth, LogoHeight) 61 | if err != nil { 62 | base64Str = repository.DefaultLogoBase64 63 | } 64 | 65 | column := gormx.ColumnName(query.SysConfig.SiteLogo) 66 | update[column] = base64Str 67 | } 68 | if req.FaviconFile != nil && req.FaviconFile.Size > 0 { 69 | base64Str, err := tools.ResizeMultipartImgToBase64(req.FaviconFile, FaviconWidth, FaviconHeight) 70 | if err != nil { 71 | base64Str = repository.DefaultFaviconBase64 72 | } 73 | 74 | column := gormx.ColumnName(query.SysConfig.SiteFavicon) 75 | update[column] = base64Str 76 | } 77 | 78 | if _, err = s.configRepo.WithContext(ctx).Update(update, s.configRepo.WhereByID(1)); err != nil { 79 | return nil, err 80 | } 81 | 82 | return nil, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/service/dashboard/dashboard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/07 20:26 4 | */ 5 | 6 | package dashboard 7 | 8 | import ( 9 | "errors" 10 | "golang.org/x/sync/errgroup" 11 | "math/big" 12 | "os" 13 | "runtime" 14 | "strings" 15 | "time" 16 | 17 | "github.com/duke-git/lancet/v2/mathutil" 18 | humanize "github.com/dustin/go-humanize" 19 | "github.com/gin-gonic/gin" 20 | "github.com/shirou/gopsutil/cpu" 21 | "github.com/shirou/gopsutil/disk" 22 | "github.com/shirou/gopsutil/mem" 23 | 24 | v1 "github.com/ch3nnn/webstack-go/api/v1" 25 | ) 26 | 27 | func (s *service) Dashboard(ctx *gin.Context) (*v1.DashboardResp, error) { 28 | var ( 29 | g errgroup.Group 30 | dir string 31 | cpuPercent float64 32 | memoryInfo *mem.VirtualMemoryStat 33 | diskInfo *disk.UsageStat 34 | cpuInfo *cpu.InfoStat 35 | ) 36 | 37 | g.Go(func() (err error) { 38 | memoryInfo, err = mem.VirtualMemoryWithContext(ctx) 39 | if err != nil { 40 | return err 41 | } 42 | return 43 | }) 44 | g.Go(func() (err error) { 45 | diskInfo, err = disk.UsageWithContext(ctx, "/") 46 | if err != nil { 47 | return err 48 | } 49 | return 50 | }) 51 | g.Go(func() (err error) { 52 | cpuInfos, err := cpu.InfoWithContext(ctx) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if len(cpuInfos) > 0 { 58 | cpuInfo = &cpuInfos[0] 59 | return 60 | } 61 | 62 | return errors.New("no cpu info") 63 | }) 64 | g.Go(func() (err error) { 65 | cpuPercents, err := cpu.PercentWithContext(ctx, time.Second, false) 66 | if len(cpuPercents) > 0 { 67 | cpuPercent = mathutil.RoundToFloat(cpuPercents[0], 2) 68 | } 69 | return 70 | }) 71 | g.Go(func() (err error) { 72 | dir, err = os.Getwd() 73 | return 74 | }) 75 | 76 | if err := g.Wait(); err != nil { 77 | return nil, err 78 | } 79 | 80 | resp := &v1.DashboardResp{ 81 | ProjectVersion: "2.0", 82 | GoOS: runtime.GOOS, 83 | GoArch: runtime.GOARCH, 84 | GoVersion: runtime.Version(), 85 | ProjectPath: strings.Replace(dir, "\\", "/", -1), 86 | MemTotal: humanize.BigBytes(big.NewInt(int64(memoryInfo.Total))), 87 | MemUsed: humanize.BigBytes(big.NewInt(int64(memoryInfo.Used))), 88 | MemUsedPercent: mathutil.RoundToFloat(memoryInfo.UsedPercent, 2), 89 | DiskTotal: humanize.BigBytes(big.NewInt(int64(diskInfo.Total))), 90 | DiskUsed: humanize.BigBytes(big.NewInt(int64(diskInfo.Used))), 91 | DiskUsedPercent: mathutil.RoundToFloat(diskInfo.UsedPercent, 2), 92 | CpuName: cpuInfo.ModelName, 93 | CpuCores: cpuInfo.Cores, 94 | CpuUsedPercent: cpuPercent, 95 | } 96 | 97 | return resp, nil 98 | } 99 | -------------------------------------------------------------------------------- /internal/service/dashboard/service.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/17 下午7:32 4 | */ 5 | 6 | package dashboard 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | s "github.com/ch3nnn/webstack-go/internal/service" 13 | ) 14 | 15 | var _ Service = (*service)(nil) 16 | 17 | type Service interface { 18 | i() 19 | 20 | Dashboard(ctx *gin.Context) (*v1.DashboardResp, error) 21 | } 22 | 23 | type service struct { 24 | *s.Service 25 | } 26 | 27 | func NewService(s *s.Service) Service { 28 | return &service{ 29 | Service: s, 30 | } 31 | } 32 | 33 | func (s *service) i() {} 34 | -------------------------------------------------------------------------------- /internal/service/dashboard/service.mockgen.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/service/dashboard/service.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=internal/service/dashboard/service.go -destination internal/service/dashboard/service.mockgen.go -package=dashboard 7 | // 8 | 9 | // Package dashboard is a generated GoMock package. 10 | package dashboard 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | v1 "github.com/ch3nnn/webstack-go/api/v1" 16 | gin "github.com/gin-gonic/gin" 17 | gomock "go.uber.org/mock/gomock" 18 | ) 19 | 20 | // MockService is a mock of Service interface. 21 | type MockService struct { 22 | ctrl *gomock.Controller 23 | recorder *MockServiceMockRecorder 24 | isgomock struct{} 25 | } 26 | 27 | // MockServiceMockRecorder is the mock recorder for MockService. 28 | type MockServiceMockRecorder struct { 29 | mock *MockService 30 | } 31 | 32 | // NewMockService creates a new mock instance. 33 | func NewMockService(ctrl *gomock.Controller) *MockService { 34 | mock := &MockService{ctrl: ctrl} 35 | mock.recorder = &MockServiceMockRecorder{mock} 36 | return mock 37 | } 38 | 39 | // EXPECT returns an object that allows the caller to indicate expected use. 40 | func (m *MockService) EXPECT() *MockServiceMockRecorder { 41 | return m.recorder 42 | } 43 | 44 | // Dashboard mocks base method. 45 | func (m *MockService) Dashboard(ctx *gin.Context) (*v1.DashboardResp, error) { 46 | m.ctrl.T.Helper() 47 | ret := m.ctrl.Call(m, "Dashboard", ctx) 48 | ret0, _ := ret[0].(*v1.DashboardResp) 49 | ret1, _ := ret[1].(error) 50 | return ret0, ret1 51 | } 52 | 53 | // Dashboard indicates an expected call of Dashboard. 54 | func (mr *MockServiceMockRecorder) Dashboard(ctx any) *gomock.Call { 55 | mr.mock.ctrl.T.Helper() 56 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dashboard", reflect.TypeOf((*MockService)(nil).Dashboard), ctx) 57 | } 58 | 59 | // i mocks base method. 60 | func (m *MockService) i() { 61 | m.ctrl.T.Helper() 62 | m.ctrl.Call(m, "i") 63 | } 64 | 65 | // i indicates an expected call of i. 66 | func (mr *MockServiceMockRecorder) i() *gomock.Call { 67 | mr.mock.ctrl.T.Helper() 68 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "i", reflect.TypeOf((*MockService)(nil).i)) 69 | } 70 | -------------------------------------------------------------------------------- /internal/service/dashboard/service_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/11 13:25 4 | */ 5 | 6 | package dashboard 7 | 8 | import ( 9 | "flag" 10 | "os" 11 | "testing" 12 | 13 | sqlmock "github.com/DATA-DOG/go-sqlmock" 14 | "github.com/gin-gonic/gin" 15 | "github.com/stretchr/testify/assert" 16 | "gorm.io/driver/mysql" 17 | "gorm.io/gorm" 18 | 19 | "github.com/ch3nnn/webstack-go/internal/dal/query" 20 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 21 | s "github.com/ch3nnn/webstack-go/internal/service" 22 | "github.com/ch3nnn/webstack-go/pkg/config" 23 | "github.com/ch3nnn/webstack-go/pkg/jwt" 24 | "github.com/ch3nnn/webstack-go/pkg/log" 25 | ) 26 | 27 | var ( 28 | logger *log.Logger 29 | j *jwt.JWT 30 | ) 31 | 32 | func setupRepository(t *testing.T) (*repository.Repository, sqlmock.Sqlmock) { 33 | mockDB, mock, err := sqlmock.New() 34 | if err != nil { 35 | t.Fatalf("failed to create sqlmock: %v", err) 36 | } 37 | 38 | db, err := gorm.Open(mysql.New(mysql.Config{ 39 | Conn: mockDB, 40 | SkipInitializeWithVersion: true, 41 | }), &gorm.Config{}) 42 | if err != nil { 43 | t.Fatalf("failed to open gorm connection: %v", err) 44 | } 45 | 46 | query.SetDefault(db) 47 | 48 | return repository.NewRepository(logger, db), mock 49 | } 50 | 51 | func TestMain(m *testing.M) { 52 | err := os.Setenv("APP_CONF", "../../../config/local.yml") 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | envConf := flag.String("conf", "config/local.yml", "config path, eg: -conf ./config/local.yml") 58 | flag.Parse() 59 | conf := config.NewConfig(*envConf) 60 | 61 | logger = log.NewLog(conf) 62 | j = jwt.NewJwt(conf) 63 | 64 | code := m.Run() 65 | os.Exit(code) 66 | } 67 | 68 | func TestDashboardService_Dashboard(t *testing.T) { 69 | ctx := &gin.Context{} 70 | repo, _ := setupRepository(t) 71 | srv := s.NewService(logger, j, repo) 72 | 73 | dashboardService := NewService(srv) 74 | _, err := dashboardService.Dashboard(ctx) 75 | 76 | assert.NoError(t, err) 77 | } 78 | -------------------------------------------------------------------------------- /internal/service/index/about.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/18 21:59 4 | */ 5 | 6 | package index 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | ) 13 | 14 | func (s *service) About(ctx *gin.Context) (*v1.AboutResp, error) { 15 | sysConfig, err := s.configRepo.WithContext(ctx).FindOne() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &v1.AboutResp{ 21 | About: v1.About{ 22 | AboutSite: sysConfig.AboutSite, 23 | AboutAuthor: sysConfig.AboutAuthor, 24 | IsAbout: sysConfig.IsAbout, 25 | }, 26 | }, nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/service/index/service.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午1:48 4 | */ 5 | 6 | package index 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 15 | s "github.com/ch3nnn/webstack-go/internal/service" 16 | ) 17 | 18 | var _ Service = (*service)(nil) 19 | 20 | type Service interface { 21 | i() 22 | 23 | // Index 首页 24 | Index(ctx context.Context) (*v1.IndexResp, error) 25 | // About 关于我 26 | About(ctx *gin.Context) (*v1.AboutResp, error) 27 | } 28 | 29 | type service struct { 30 | *s.Service 31 | siteRepo repository.IStSiteDao 32 | categoryRepo repository.IStCategoryDao 33 | configRepo repository.ISysConfigDao 34 | } 35 | 36 | func NewService( 37 | s *s.Service, 38 | siteRepo repository.IStSiteDao, 39 | categoryRepo repository.IStCategoryDao, 40 | configRepo repository.ISysConfigDao, 41 | ) Service { 42 | return &service{ 43 | Service: s, 44 | siteRepo: siteRepo, 45 | categoryRepo: categoryRepo, 46 | configRepo: configRepo, 47 | } 48 | } 49 | 50 | func (s *service) i() {} 51 | -------------------------------------------------------------------------------- /internal/service/index/service.mockgen.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/service/index/service.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=internal/service/index/service.go -destination internal/service/index/service.mockgen.go -package=index 7 | // 8 | 9 | // Package index is a generated GoMock package. 10 | package index 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | v1 "github.com/ch3nnn/webstack-go/api/v1" 17 | gin "github.com/gin-gonic/gin" 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockService is a mock of Service interface. 22 | type MockService struct { 23 | ctrl *gomock.Controller 24 | recorder *MockServiceMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockServiceMockRecorder is the mock recorder for MockService. 29 | type MockServiceMockRecorder struct { 30 | mock *MockService 31 | } 32 | 33 | // NewMockService creates a new mock instance. 34 | func NewMockService(ctrl *gomock.Controller) *MockService { 35 | mock := &MockService{ctrl: ctrl} 36 | mock.recorder = &MockServiceMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockService) EXPECT() *MockServiceMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // About mocks base method. 46 | func (m *MockService) About(ctx *gin.Context) (*v1.AboutResp, error) { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "About", ctx) 49 | ret0, _ := ret[0].(*v1.AboutResp) 50 | ret1, _ := ret[1].(error) 51 | return ret0, ret1 52 | } 53 | 54 | // About indicates an expected call of About. 55 | func (mr *MockServiceMockRecorder) About(ctx any) *gomock.Call { 56 | mr.mock.ctrl.T.Helper() 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "About", reflect.TypeOf((*MockService)(nil).About), ctx) 58 | } 59 | 60 | // Index mocks base method. 61 | func (m *MockService) Index(ctx context.Context) (*v1.IndexResp, error) { 62 | m.ctrl.T.Helper() 63 | ret := m.ctrl.Call(m, "Index", ctx) 64 | ret0, _ := ret[0].(*v1.IndexResp) 65 | ret1, _ := ret[1].(error) 66 | return ret0, ret1 67 | } 68 | 69 | // Index indicates an expected call of Index. 70 | func (mr *MockServiceMockRecorder) Index(ctx any) *gomock.Call { 71 | mr.mock.ctrl.T.Helper() 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index", reflect.TypeOf((*MockService)(nil).Index), ctx) 73 | } 74 | 75 | // i mocks base method. 76 | func (m *MockService) i() { 77 | m.ctrl.T.Helper() 78 | m.ctrl.Call(m, "i") 79 | } 80 | 81 | // i indicates an expected call of i. 82 | func (mr *MockServiceMockRecorder) i() *gomock.Call { 83 | mr.mock.ctrl.T.Helper() 84 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "i", reflect.TypeOf((*MockService)(nil).i)) 85 | } 86 | -------------------------------------------------------------------------------- /internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 5 | "github.com/ch3nnn/webstack-go/pkg/jwt" 6 | "github.com/ch3nnn/webstack-go/pkg/log" 7 | ) 8 | 9 | type Service struct { 10 | Logger *log.Logger 11 | Jwt *jwt.JWT 12 | Repository *repository.Repository 13 | } 14 | 15 | func NewService( 16 | logger *log.Logger, 17 | jwt *jwt.JWT, 18 | repository *repository.Repository, 19 | ) *Service { 20 | return &Service{ 21 | Logger: logger, 22 | Jwt: jwt, 23 | Repository: repository, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/service/site/batchcreate.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/04 下午4:33 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "context" 10 | "strings" 11 | 12 | "github.com/duke-git/lancet/v2/condition" 13 | "github.com/duke-git/lancet/v2/validator" 14 | "golang.org/x/sync/errgroup" 15 | 16 | v1 "github.com/ch3nnn/webstack-go/api/v1" 17 | "github.com/ch3nnn/webstack-go/internal/dal/model" 18 | "github.com/ch3nnn/webstack-go/pkg/tools" 19 | ) 20 | 21 | func (s *service) parseURL(u string) (urls []string) { 22 | for _, u := range strings.Split(u, "\n") { 23 | if validator.IsUrl(u) || validator.IsIp(u) || validator.IsIpPort(u) { 24 | urls = append(urls, u) 25 | } 26 | } 27 | 28 | return 29 | } 30 | 31 | func (s *service) BatchCreate(ctx context.Context, req *v1.SiteCreateReq) (*v1.SiteCreateResp, error) { 32 | workerPool := tools.NewWorkerPool(5, 20) 33 | workerPool.Start() 34 | 35 | var successCnt int 36 | var failURLs []string 37 | for _, u := range s.parseURL(req.Url) { 38 | workerPool.AddJob(func() { 39 | var ( 40 | g errgroup.Group 41 | title, icon, desc string 42 | ) 43 | 44 | u = strings.TrimSpace(u) 45 | 46 | g.Go(func() (err error) { 47 | title, err = getWebTitle(u) 48 | return 49 | }) 50 | g.Go(func() (err error) { 51 | icon, err = getWebLogoIconBase64(u) 52 | return 53 | }) 54 | g.Go(func() (err error) { 55 | desc, err = getWebDescription(u) 56 | return 57 | }) 58 | 59 | if err := g.Wait(); err != nil { 60 | if !req.FailSwitch { 61 | failURLs = append(failURLs, u) 62 | return 63 | } 64 | } 65 | 66 | _, err := s.siteRepository.WithContext(ctx).Create(&model.StSite{ 67 | Title: condition.Ternary(title != "", title, u), 68 | Icon: icon, 69 | Description: desc, 70 | URL: u, 71 | CategoryID: req.CategoryID, 72 | IsUsed: req.IsUsed, 73 | Sort: 0, 74 | }) 75 | if err != nil { 76 | failURLs = append(failURLs, u) 77 | return 78 | } 79 | 80 | successCnt++ 81 | }) 82 | } 83 | 84 | workerPool.Wait() 85 | 86 | return &v1.SiteCreateResp{ 87 | FailCount: len(failURLs), 88 | SuccessCount: successCnt, 89 | FailURLs: failURLs, 90 | }, nil 91 | } 92 | -------------------------------------------------------------------------------- /internal/service/site/delete.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/10 上午12:20 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "context" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | ) 13 | 14 | func (s *service) Delete(ctx context.Context, req *v1.SiteDeleteReq) (resp *v1.SiteDeleteResp, err error) { 15 | err = s.siteRepository.WithContext(ctx).Delete(s.siteRepository.WhereByID(req.ID)) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | return &v1.SiteDeleteResp{ID: req.ID}, nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/service/site/export.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/29 19:10 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "strconv" 10 | 11 | "github.com/gin-gonic/gin" 12 | excelize "github.com/xuri/excelize/v2" 13 | "gorm.io/gen" 14 | "gorm.io/gen/field" 15 | 16 | v1 "github.com/ch3nnn/webstack-go/api/v1" 17 | "github.com/ch3nnn/webstack-go/internal/dal/query" 18 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 19 | ) 20 | 21 | var ( 22 | sheetName = "Sheet1" 23 | headers = []string{"ID", "Logo", "名称简介", "链接", "分类", "创建日期", "更新日期", "状态"} 24 | ) 25 | 26 | func (s *service) Export(ctx *gin.Context, req *v1.SiteExportReq) (resp *v1.SiteExportResp, err error) { 27 | var orderColumns []field.Expr 28 | orderColumns = append(orderColumns, query.StSite.CreatedAt.Desc()) 29 | 30 | var whereFunc []func(dao gen.Dao) gen.Dao 31 | if req.Search != "" { 32 | whereFunc = append(whereFunc, s.siteRepository.LikeInByTitleOrDescOrURL(req.Search)) 33 | } 34 | if req.CategoryID != 0 { 35 | whereFunc = append(whereFunc, s.siteRepository.WhereByCategoryID(req.CategoryID)) 36 | orderColumns = []field.Expr{query.StSite.Sort.Asc()} 37 | } 38 | 39 | var siteCategories []repository.SiteCategory 40 | _, err = s.siteRepository.WithContext(ctx).FindSiteCategoryWithPage(1, 10000, &siteCategories, orderColumns, whereFunc...) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | excelFile := excelize.NewFile() 46 | 47 | for i, header := range headers { 48 | cell, _ := excelize.CoordinatesToCellName(i+1, 1) 49 | if err := excelFile.SetCellValue(sheetName, cell, header); err != nil { 50 | continue 51 | } 52 | } 53 | 54 | for i, siteCategory := range siteCategories { 55 | row := strconv.Itoa(i + 2) 56 | 57 | excelFile.SetCellValue(sheetName, "A"+row, siteCategory.StSite.ID) 58 | excelFile.SetCellValue(sheetName, "B"+row, siteCategory.StSite.Icon) 59 | excelFile.SetCellValue(sheetName, "C"+row, siteCategory.StSite.Title) 60 | excelFile.SetCellValue(sheetName, "D"+row, siteCategory.StSite.URL) 61 | excelFile.SetCellValue(sheetName, "E"+row, siteCategory.StCategory.Title) 62 | excelFile.SetCellValue(sheetName, "F"+row, siteCategory.StSite.CreatedAt) 63 | excelFile.SetCellValue(sheetName, "G"+row, siteCategory.StSite.UpdatedAt) 64 | excelFile.SetCellValue(sheetName, "H"+row, siteCategory.StSite.IsUsed) 65 | } 66 | 67 | index, err := excelFile.NewSheet(sheetName) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | excelFile.SetActiveSheet(index) 73 | 74 | return &v1.SiteExportResp{File: excelFile}, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/service/site/list.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/27 下午5:58 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "context" 10 | "time" 11 | 12 | "gorm.io/gen" 13 | "gorm.io/gen/field" 14 | 15 | v1 "github.com/ch3nnn/webstack-go/api/v1" 16 | "github.com/ch3nnn/webstack-go/internal/dal/query" 17 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 18 | ) 19 | 20 | func (s *service) List(ctx context.Context, req *v1.SiteListReq) (resp *v1.SiteListResp, err error) { 21 | var orderColumns []field.Expr 22 | orderColumns = append(orderColumns, query.StSite.CreatedAt.Desc()) 23 | 24 | var whereFunc []func(dao gen.Dao) gen.Dao 25 | if req.Search != "" { 26 | whereFunc = append(whereFunc, s.siteRepository.LikeInByTitleOrDescOrURL(req.Search)) 27 | } 28 | if req.CategoryID != 0 { 29 | whereFunc = append(whereFunc, s.siteRepository.WhereByCategoryID(req.CategoryID)) 30 | orderColumns = []field.Expr{query.StSite.Sort.Asc()} // 同分类网址按排序升序 31 | } 32 | 33 | var siteCategories []repository.SiteCategory 34 | count, err := s.siteRepository.WithContext(ctx).FindSiteCategoryWithPage(req.Page, req.PageSize, &siteCategories, orderColumns, whereFunc...) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | list := make([]v1.Site, len(siteCategories)) 40 | for i, siteCategory := range siteCategories { 41 | list[i] = v1.Site{ 42 | Id: siteCategory.StSite.ID, 43 | Icon: siteCategory.StSite.Icon, 44 | Title: siteCategory.StSite.Title, 45 | Url: siteCategory.StSite.URL, 46 | Category: siteCategory.StCategory.Title, 47 | CategoryId: siteCategory.StSite.CategoryID, 48 | Description: siteCategory.StSite.Description, 49 | IsUsed: siteCategory.StSite.IsUsed, 50 | Sort: siteCategory.StSite.Sort, 51 | CreatedAt: siteCategory.StSite.CreatedAt.Format(time.DateTime), 52 | UpdatedAt: siteCategory.StSite.UpdatedAt.Format(time.DateTime), 53 | } 54 | } 55 | 56 | return &v1.SiteListResp{ 57 | List: list, 58 | Pagination: v1.SiteLisPagination{ 59 | Total: count, 60 | CurrentPage: req.Page, 61 | PerPageCount: req.PageSize, 62 | }, 63 | }, err 64 | } 65 | -------------------------------------------------------------------------------- /internal/service/site/sync.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/11/12 16:40 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "strings" 10 | 11 | "github.com/gin-gonic/gin" 12 | "golang.org/x/sync/errgroup" 13 | 14 | v1 "github.com/ch3nnn/webstack-go/api/v1" 15 | "github.com/ch3nnn/webstack-go/internal/dal/model" 16 | ) 17 | 18 | func (s *service) Sync(ctx *gin.Context, req *v1.SiteSyncReq) (resp *v1.SiteSyncResp, err error) { 19 | var ( 20 | g errgroup.Group 21 | title, icon, desc string 22 | ) 23 | 24 | site, err := s.siteRepository.WithContext(ctx).FindOne(s.siteRepository.WhereByID(req.ID)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | url := strings.TrimSpace(site.URL) 30 | 31 | g.Go(func() (err error) { 32 | title, err = getWebTitle(url) 33 | return 34 | }) 35 | g.Go(func() (err error) { 36 | icon, err = getWebLogoIconBase64(url) 37 | return 38 | }) 39 | g.Go(func() (err error) { 40 | desc, err = getWebDescription(url) 41 | return 42 | }) 43 | 44 | if err := g.Wait(); err != nil { 45 | return nil, err 46 | } 47 | 48 | _, err = s.siteRepository.WithContext(ctx).Update(&model.StSite{ 49 | Title: title, 50 | Icon: icon, 51 | Description: desc, 52 | IsUsed: false, 53 | }, 54 | s.siteRepository.WhereByID(req.ID), 55 | ) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return &v1.SiteSyncResp{ID: site.ID}, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/service/site/update.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/06/30 下午10:14 4 | */ 5 | 6 | package site 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | 11 | v1 "github.com/ch3nnn/webstack-go/api/v1" 12 | "github.com/ch3nnn/webstack-go/internal/dal/query" 13 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 14 | "github.com/ch3nnn/webstack-go/pkg/gormx" 15 | "github.com/ch3nnn/webstack-go/pkg/tools" 16 | ) 17 | 18 | const ( 19 | FaviconWidth = 64 20 | FaviconHeight = 64 21 | ) 22 | 23 | func (s *service) Update(ctx *gin.Context, req *v1.SiteUpdateReq) (resp *v1.SiteUpdateResp, err error) { 24 | update := make(map[string]any) 25 | 26 | if req.CategoryId != 0 { 27 | column := gormx.ColumnName(query.StSite.CategoryID) 28 | update[column] = req.CategoryId 29 | } 30 | if req.Title != "" { 31 | column := gormx.ColumnName(query.StSite.Title) 32 | update[column] = req.Title 33 | } 34 | if req.Icon != "" { 35 | base64Str, err := tools.ResizeURLImgToBase64(req.Icon, FaviconWidth, FaviconHeight) 36 | if err != nil { 37 | base64Str = repository.DefaultFaviconBase64 38 | } 39 | 40 | column := gormx.ColumnName(query.StSite.Icon) 41 | update[column] = base64Str 42 | } 43 | if req.File != nil && req.File.Size > 0 { 44 | base64Str, err := tools.ResizeMultipartImgToBase64(req.File, FaviconWidth, FaviconHeight) 45 | if err != nil { 46 | base64Str = repository.DefaultFaviconBase64 47 | } 48 | 49 | column := gormx.ColumnName(query.StSite.Icon) 50 | update[column] = base64Str 51 | } 52 | if req.Description != "" { 53 | column := gormx.ColumnName(query.StSite.Description) 54 | update[column] = req.Description 55 | } 56 | if req.Url != "" { 57 | column := gormx.ColumnName(query.StSite.URL) 58 | update[column] = req.Url 59 | } 60 | if req.IsUsed != nil { 61 | column := gormx.ColumnName(query.StSite.IsUsed) 62 | update[column] = req.IsUsed 63 | } 64 | if req.Sort >= 0 { 65 | column := gormx.ColumnName(query.StSite.Sort) 66 | update[column] = req.Sort 67 | } 68 | 69 | _, err = s.siteRepository.WithContext(ctx).Update(update, s.siteRepository.WhereByID(req.Id)) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return &v1.SiteUpdateResp{ID: req.Id}, nil 75 | } 76 | -------------------------------------------------------------------------------- /internal/service/user/info.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 下午3:51 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "github.com/gin-gonic/gin" 10 | "golang.org/x/sync/errgroup" 11 | 12 | v1 "github.com/ch3nnn/webstack-go/api/v1" 13 | "github.com/ch3nnn/webstack-go/internal/dal/model" 14 | "github.com/ch3nnn/webstack-go/internal/middleware" 15 | ) 16 | 17 | func (s *service) Info(ctx *gin.Context, _ *v1.InfoReq) (*v1.InfoResp, error) { 18 | var ( 19 | g errgroup.Group 20 | user *model.SysUser 21 | menus []*model.SysMenu 22 | adminMenus []*model.SysUserMenu 23 | ) 24 | 25 | g.Go(func() (err error) { 26 | user, err = s.userRepo.WithContext(ctx).FindOne(s.userRepo.WhereByID(ctx.GetInt(middleware.UserID))) 27 | if err != nil { 28 | return err 29 | } 30 | return nil 31 | }) 32 | 33 | g.Go(func() (err error) { 34 | menus, err = s.menuRepo.WithContext(ctx).FindAll() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | }) 41 | 42 | g.Go(func() (err error) { 43 | adminMenus, err = s.adminMenuRepo.WithContext(ctx).FindAll(s.adminMenuRepo.WhereByUserID(ctx.GetInt(middleware.UserID))) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | }) 50 | 51 | if err := g.Wait(); err != nil { 52 | return nil, err 53 | } 54 | 55 | var menuList []v1.Menu 56 | for _, menu := range menus { 57 | for _, adminMenu := range adminMenus { 58 | if menu.ID == adminMenu.MenuID { 59 | menuList = append(menuList, v1.Menu{ 60 | Id: menu.ID, 61 | Pid: menu.Pid, 62 | Name: menu.Name, 63 | Link: menu.Link, 64 | Icon: menu.Icon, 65 | }) 66 | } 67 | } 68 | } 69 | 70 | return &v1.InfoResp{ 71 | Username: user.Username, 72 | Menus: menuList, 73 | }, nil 74 | } 75 | -------------------------------------------------------------------------------- /internal/service/user/login.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午12:27 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "context" 10 | "time" 11 | 12 | v1 "github.com/ch3nnn/webstack-go/api/v1" 13 | ) 14 | 15 | func (s *service) Login(ctx context.Context, req *v1.LoginReq) (resp *v1.LoginResp, err error) { 16 | user, err := s.userRepo.WithContext(ctx). 17 | FindOne( 18 | s.userRepo.WhereByUsername(req.Username), 19 | s.userRepo.WhereByPassword(req.Password), 20 | ) 21 | if err != nil { 22 | return nil, v1.ErrorUserNameAndPassword 23 | } 24 | 25 | token, err := s.Jwt.GenToken(user.ID, time.Now().Add(time.Hour*24)) 26 | if err != nil { 27 | return nil, v1.ErrorTokenGeneration 28 | } 29 | 30 | return &v1.LoginResp{Token: token}, nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/service/user/service.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/05/26 上午12:26 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "context" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | v1 "github.com/ch3nnn/webstack-go/api/v1" 14 | "github.com/ch3nnn/webstack-go/internal/dal/repository" 15 | s "github.com/ch3nnn/webstack-go/internal/service" 16 | ) 17 | 18 | var _ Service = (*service)(nil) 19 | 20 | type Service interface { 21 | i() 22 | 23 | // Info 获取用户信息 24 | Info(ctx *gin.Context, req *v1.InfoReq) (*v1.InfoResp, error) 25 | // Login 登录 26 | Login(ctx context.Context, req *v1.LoginReq) (resp *v1.LoginResp, err error) 27 | // UpdatePassword 修改密码 28 | UpdatePassword(ctx *gin.Context, req *v1.UpdatePasswordReq) (*v1.UpdatePasswordResp, error) 29 | } 30 | 31 | type service struct { 32 | *s.Service 33 | userRepo repository.ISysUserDao 34 | siteRepo repository.IStSiteDao 35 | categoryRepo repository.IStCategoryDao 36 | menuRepo repository.ISysMenuDao 37 | adminMenuRepo repository.ISysUserMenuDao 38 | } 39 | 40 | func NewService( 41 | s *s.Service, 42 | userRepo repository.ISysUserDao, 43 | siteRepo repository.IStSiteDao, 44 | categoryRepo repository.IStCategoryDao, 45 | menuRepo repository.ISysMenuDao, 46 | adminMenuRepo repository.ISysUserMenuDao, 47 | ) Service { 48 | return &service{ 49 | Service: s, 50 | userRepo: userRepo, 51 | siteRepo: siteRepo, 52 | categoryRepo: categoryRepo, 53 | menuRepo: menuRepo, 54 | adminMenuRepo: adminMenuRepo, 55 | } 56 | } 57 | 58 | func (s *service) i() {} 59 | -------------------------------------------------------------------------------- /internal/service/user/service.mockgen.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: internal/service/user/service.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source=internal/service/user/service.go -destination internal/service/user/service.mockgen.go -package=user 7 | // 8 | 9 | // Package user is a generated GoMock package. 10 | package user 11 | 12 | import ( 13 | context "context" 14 | reflect "reflect" 15 | 16 | v1 "github.com/ch3nnn/webstack-go/api/v1" 17 | gin "github.com/gin-gonic/gin" 18 | gomock "go.uber.org/mock/gomock" 19 | ) 20 | 21 | // MockService is a mock of Service interface. 22 | type MockService struct { 23 | ctrl *gomock.Controller 24 | recorder *MockServiceMockRecorder 25 | isgomock struct{} 26 | } 27 | 28 | // MockServiceMockRecorder is the mock recorder for MockService. 29 | type MockServiceMockRecorder struct { 30 | mock *MockService 31 | } 32 | 33 | // NewMockService creates a new mock instance. 34 | func NewMockService(ctrl *gomock.Controller) *MockService { 35 | mock := &MockService{ctrl: ctrl} 36 | mock.recorder = &MockServiceMockRecorder{mock} 37 | return mock 38 | } 39 | 40 | // EXPECT returns an object that allows the caller to indicate expected use. 41 | func (m *MockService) EXPECT() *MockServiceMockRecorder { 42 | return m.recorder 43 | } 44 | 45 | // Info mocks base method. 46 | func (m *MockService) Info(ctx *gin.Context, req *v1.InfoReq) (*v1.InfoResp, error) { 47 | m.ctrl.T.Helper() 48 | ret := m.ctrl.Call(m, "Info", ctx, req) 49 | ret0, _ := ret[0].(*v1.InfoResp) 50 | ret1, _ := ret[1].(error) 51 | return ret0, ret1 52 | } 53 | 54 | // Info indicates an expected call of Info. 55 | func (mr *MockServiceMockRecorder) Info(ctx, req any) *gomock.Call { 56 | mr.mock.ctrl.T.Helper() 57 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockService)(nil).Info), ctx, req) 58 | } 59 | 60 | // Login mocks base method. 61 | func (m *MockService) Login(ctx context.Context, req *v1.LoginReq) (*v1.LoginResp, error) { 62 | m.ctrl.T.Helper() 63 | ret := m.ctrl.Call(m, "Login", ctx, req) 64 | ret0, _ := ret[0].(*v1.LoginResp) 65 | ret1, _ := ret[1].(error) 66 | return ret0, ret1 67 | } 68 | 69 | // Login indicates an expected call of Login. 70 | func (mr *MockServiceMockRecorder) Login(ctx, req any) *gomock.Call { 71 | mr.mock.ctrl.T.Helper() 72 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockService)(nil).Login), ctx, req) 73 | } 74 | 75 | // UpdatePassword mocks base method. 76 | func (m *MockService) UpdatePassword(ctx *gin.Context, req *v1.UpdatePasswordReq) (*v1.UpdatePasswordResp, error) { 77 | m.ctrl.T.Helper() 78 | ret := m.ctrl.Call(m, "UpdatePassword", ctx, req) 79 | ret0, _ := ret[0].(*v1.UpdatePasswordResp) 80 | ret1, _ := ret[1].(error) 81 | return ret0, ret1 82 | } 83 | 84 | // UpdatePassword indicates an expected call of UpdatePassword. 85 | func (mr *MockServiceMockRecorder) UpdatePassword(ctx, req any) *gomock.Call { 86 | mr.mock.ctrl.T.Helper() 87 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePassword", reflect.TypeOf((*MockService)(nil).UpdatePassword), ctx, req) 88 | } 89 | 90 | // i mocks base method. 91 | func (m *MockService) i() { 92 | m.ctrl.T.Helper() 93 | m.ctrl.Call(m, "i") 94 | } 95 | 96 | // i indicates an expected call of i. 97 | func (mr *MockServiceMockRecorder) i() *gomock.Call { 98 | mr.mock.ctrl.T.Helper() 99 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "i", reflect.TypeOf((*MockService)(nil).i)) 100 | } 101 | -------------------------------------------------------------------------------- /internal/service/user/updatepassword.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2024/11/11 18:40 4 | */ 5 | 6 | package user 7 | 8 | import ( 9 | "errors" 10 | 11 | "github.com/gin-gonic/gin" 12 | "gorm.io/gorm" 13 | 14 | v1 "github.com/ch3nnn/webstack-go/api/v1" 15 | "github.com/ch3nnn/webstack-go/internal/dal/model" 16 | "github.com/ch3nnn/webstack-go/internal/middleware" 17 | ) 18 | 19 | func (s *service) UpdatePassword(ctx *gin.Context, req *v1.UpdatePasswordReq) (*v1.UpdatePasswordResp, error) { 20 | user, err := s.userRepo.WithContext(ctx). 21 | FindOne( 22 | s.userRepo.WhereByID(ctx.GetInt(middleware.UserID)), 23 | ) 24 | if err != nil { 25 | if errors.Is(err, gorm.ErrRecordNotFound) { 26 | return nil, errors.Join(errors.New("用户ID"), v1.ErrNotFound) 27 | } 28 | return nil, err 29 | } 30 | 31 | if user.Password != req.OldPassword { 32 | return nil, v1.ErrorUserOldPassword 33 | } 34 | 35 | _, err = s.userRepo.WithContext(ctx).Update(&model.SysUser{Password: req.NewPassword}, s.userRepo.WhereByID(user.ID)) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return nil, nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/ch3nnn/webstack-go/pkg/server" 11 | ) 12 | 13 | type App struct { 14 | name string 15 | servers []server.Server 16 | } 17 | 18 | type Option func(a *App) 19 | 20 | func NewApp(opts ...Option) *App { 21 | a := &App{} 22 | for _, opt := range opts { 23 | opt(a) 24 | } 25 | return a 26 | } 27 | 28 | func WithServer(servers ...server.Server) Option { 29 | return func(a *App) { 30 | a.servers = servers 31 | } 32 | } 33 | 34 | func WithName(name string) Option { 35 | return func(a *App) { 36 | a.name = name 37 | } 38 | } 39 | 40 | func (a *App) Run(ctx context.Context) error { 41 | ctx, cancel := context.WithCancel(ctx) 42 | defer cancel() 43 | 44 | signals := make(chan os.Signal, 1) 45 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) 46 | 47 | for _, srv := range a.servers { 48 | go func(srv server.Server) { 49 | if err := srv.Start(ctx); err != nil { 50 | log.Printf("Server start err: %v", err) 51 | } 52 | }(srv) 53 | } 54 | 55 | select { 56 | case <-signals: 57 | // Received termination signal 58 | log.Println("Received termination signal") 59 | case <-ctx.Done(): 60 | // Context canceled 61 | log.Println("Context canceled") 62 | } 63 | 64 | // Gracefully stop the servers 65 | for _, srv := range a.servers { 66 | if err := srv.Stop(ctx); err != nil { 67 | log.Printf("Server stop err: %v", err) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/spf13/viper" 8 | ) 9 | 10 | func NewConfig(p string) *viper.Viper { 11 | conf := viper.New() 12 | conf.AutomaticEnv() 13 | 14 | envConf := conf.GetString("APP_CONF") 15 | if envConf != "" { 16 | p = envConf 17 | } 18 | 19 | if _, err := os.Stat(p); os.IsNotExist(err) { 20 | panic(errors.Errorf("config file not found: %s", p)) 21 | } 22 | 23 | conf.SetConfigFile(p) 24 | 25 | if err := conf.ReadInConfig(); err != nil { 26 | panic(errors.Errorf("failed to read config file: %s", err)) 27 | } 28 | 29 | return conf 30 | } 31 | -------------------------------------------------------------------------------- /pkg/gormx/db.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/03 11:33 4 | */ 5 | 6 | package gormx 7 | 8 | const ( 9 | MYSQL = "mysql" 10 | POSTGRES = "postgres" 11 | SQLITE = "sqlite" 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/gormx/field.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/31 12:12 4 | */ 5 | 6 | package gormx 7 | 8 | import ( 9 | "strings" 10 | 11 | "gorm.io/gen/field" 12 | ) 13 | 14 | func FieldIsDesc(field string) (bool, string) { 15 | if strings.HasPrefix(field, "-") { 16 | return true, field[1:] 17 | } 18 | return false, field 19 | } 20 | 21 | func LikeInner(s string) string { 22 | return "%" + s + "%" 23 | } 24 | 25 | func LikeLeft(s string) string { 26 | return "%" + s 27 | } 28 | 29 | func LikeRight(s string) string { 30 | return s + "%" 31 | } 32 | 33 | // ColumnName 获取 model 列名 34 | func ColumnName(field field.IColumnName) string { 35 | return field.ColumnName().String() 36 | } 37 | -------------------------------------------------------------------------------- /pkg/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "time" 7 | 8 | jwt "github.com/golang-jwt/jwt/v5" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type ( 13 | JWT struct { 14 | key []byte 15 | } 16 | 17 | MyCustomClaims struct { 18 | UserID int 19 | jwt.RegisteredClaims 20 | } 21 | ) 22 | 23 | func NewJwt(conf *viper.Viper) *JWT { 24 | return &JWT{key: []byte(conf.GetString("security.jwt.key"))} 25 | } 26 | 27 | func (j *JWT) GenToken(userID int, expiresAt time.Time) (string, error) { 28 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, MyCustomClaims{ 29 | UserID: userID, 30 | RegisteredClaims: jwt.RegisteredClaims{ 31 | ExpiresAt: jwt.NewNumericDate(expiresAt), 32 | IssuedAt: jwt.NewNumericDate(time.Now()), 33 | NotBefore: jwt.NewNumericDate(time.Now()), 34 | }, 35 | }) 36 | 37 | // Sign and get the complete encoded token as a string using the key 38 | tokenString, err := token.SignedString(j.key) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return tokenString, nil 44 | } 45 | 46 | func (j *JWT) ParseToken(tokenString string) (*MyCustomClaims, error) { 47 | if strings.TrimSpace(tokenString) == "" { 48 | return nil, errors.New("token is empty") 49 | } 50 | 51 | token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { 52 | return j.key, nil 53 | }) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { 59 | return claims, nil 60 | } 61 | 62 | return nil, err 63 | } 64 | -------------------------------------------------------------------------------- /pkg/server/http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | 12 | "github.com/ch3nnn/webstack-go/pkg/log" 13 | ) 14 | 15 | func NewGinDefaultServer() *gin.Engine { 16 | gin.ForceConsoleColor() 17 | return gin.Default() 18 | } 19 | 20 | type Server struct { 21 | *gin.Engine 22 | httpSrv *http.Server 23 | host string 24 | port int 25 | logger *log.Logger 26 | } 27 | 28 | type Option func(s *Server) 29 | 30 | func NewServer(engine *gin.Engine, logger *log.Logger, opts ...Option) *Server { 31 | s := &Server{ 32 | Engine: engine, 33 | logger: logger, 34 | } 35 | for _, opt := range opts { 36 | opt(s) 37 | } 38 | return s 39 | } 40 | 41 | func WithServerHost(host string) Option { 42 | return func(s *Server) { 43 | s.host = host 44 | } 45 | } 46 | 47 | func WithServerPort(port int) Option { 48 | return func(s *Server) { 49 | s.port = port 50 | } 51 | } 52 | 53 | func (s *Server) Start(ctx context.Context) error { 54 | s.httpSrv = &http.Server{ 55 | Addr: fmt.Sprintf("%s:%d", s.host, s.port), 56 | Handler: s, 57 | } 58 | 59 | if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 60 | s.logger.Sugar().Fatalf("listen: %s\n", err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (s *Server) Stop(ctx context.Context) error { 67 | s.logger.Sugar().Info("Shutting down server...") 68 | 69 | // The context is used to inform the server it has 5 seconds to finish 70 | // the request it is currently handling 71 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 72 | defer cancel() 73 | if err := s.httpSrv.Shutdown(ctx); err != nil { 74 | s.logger.Sugar().Fatal("Server forced to shutdown: ", err) 75 | } 76 | 77 | s.logger.Sugar().Info("Server exiting") 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | ) 7 | 8 | type Server interface { 9 | Start(context.Context) error 10 | Stop(context.Context) error 11 | } 12 | 13 | // Endpointer is registry endpoint. 14 | type Endpointer interface { 15 | Endpoint() (*url.URL, error) 16 | } 17 | -------------------------------------------------------------------------------- /pkg/sid/convert.go: -------------------------------------------------------------------------------- 1 | package sid 2 | 3 | const ( 4 | base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 5 | ) 6 | 7 | func IntToBase62(n int) string { 8 | if n == 0 { 9 | return string(base62[0]) 10 | } 11 | 12 | var result []byte 13 | for n > 0 { 14 | result = append(result, base62[n%62]) 15 | n /= 62 16 | } 17 | 18 | // 反转字符串 19 | for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 { 20 | result[i], result[j] = result[j], result[i] 21 | } 22 | 23 | return string(result) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/sid/sid.go: -------------------------------------------------------------------------------- 1 | package sid 2 | 3 | import ( 4 | "github.com/sony/sonyflake" 5 | ) 6 | 7 | type Sid struct { 8 | sf *sonyflake.Sonyflake 9 | } 10 | 11 | func NewSid() *Sid { 12 | sf := sonyflake.NewSonyflake(sonyflake.Settings{}) 13 | if sf == nil { 14 | panic("sonyflake not created") 15 | } 16 | return &Sid{sf} 17 | } 18 | 19 | func (s Sid) GenString() (string, error) { 20 | id, err := s.sf.NextID() 21 | if err != nil { 22 | return "", err 23 | } 24 | return IntToBase62(int(id)), nil 25 | } 26 | 27 | func (s Sid) GenUint64() (uint64, error) { 28 | return s.sf.NextID() 29 | } 30 | -------------------------------------------------------------------------------- /pkg/tools/besticon.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/08 14:31 4 | */ 5 | 6 | package tools 7 | 8 | import ( 9 | "net/http" 10 | ) 11 | 12 | var _ http.RoundTripper = (*httpTransport)(nil) 13 | 14 | type httpTransport struct { 15 | transport http.RoundTripper 16 | 17 | userAgent string 18 | } 19 | 20 | func (h *httpTransport) RoundTrip(req *http.Request) (*http.Response, error) { 21 | req.Header.Set("Accept", "*/*") 22 | req.Header.Set("User-Agent", h.userAgent) 23 | return h.transport.RoundTrip(req) 24 | } 25 | 26 | func NewHTTPTransport(transport *http.Transport) http.RoundTripper { 27 | return &httpTransport{ 28 | transport: transport, 29 | userAgent: RandomUserAgent(), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/tools/colly.go: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gocolly/colly" 10 | "github.com/gocolly/colly/extensions" 11 | ) 12 | 13 | func NewColly() *colly.Collector { 14 | collector := colly.NewCollector( 15 | colly.DetectCharset(), // 检测响应编码 16 | colly.IgnoreRobotsTxt(), // 忽略 robots 协议 17 | ) 18 | 19 | // HTTP 设置 20 | collector.WithTransport(&http.Transport{ 21 | MaxIdleConns: 100, // 最大空闲连接数 22 | IdleConnTimeout: 5 * time.Second, // 空闲连接超时 23 | TLSHandshakeTimeout: 5 * time.Second, // TLS 握手超时 24 | ExpectContinueTimeout: 1 * time.Second, 25 | DisableKeepAlives: true, // 关闭 keepalive 26 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 不安全的跳过验证 27 | Proxy: http.ProxyFromEnvironment, 28 | DialContext: (&net.Dialer{ 29 | Timeout: 3 * time.Second, // 超时时间 30 | KeepAlive: 30 * time.Second, // KeepAlive 超时时间 31 | }).DialContext, 32 | }) 33 | 34 | // 随机 user agent 请求头 35 | extensions.RandomUserAgent(collector) 36 | extensions.Referer(collector) 37 | return collector 38 | } 39 | -------------------------------------------------------------------------------- /pkg/tools/img.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/08 12:37 4 | */ 5 | 6 | package tools 7 | 8 | import ( 9 | "bytes" 10 | "crypto/tls" 11 | "encoding/base64" 12 | "io" 13 | "mime/multipart" 14 | "net/http" 15 | 16 | "github.com/disintegration/imaging" 17 | ) 18 | 19 | func resizeImg2Base64(r io.Reader, width, height int) (base64Str string, err error) { 20 | img, err := imaging.Decode(r, imaging.AutoOrientation(true)) 21 | if err != nil { 22 | return 23 | } 24 | 25 | var buf bytes.Buffer 26 | resize := imaging.Resize(img, width, height, imaging.Lanczos) 27 | if err = imaging.Encode(&buf, resize, imaging.PNG); err != nil { 28 | return 29 | } 30 | 31 | base64Str = base64.StdEncoding.EncodeToString(buf.Bytes()) 32 | 33 | return 34 | } 35 | 36 | // ResizeMultipartImgToBase64 将multipart.FileHeader表示的图片文件调整大小,并以base64编码字符串的形式返回。 37 | // 参数f是包含图片文件信息的multipart.FileHeader指针; 38 | // 参数width和height分别是目标图片的宽度和高度。 39 | // 返回值base64Str是调整大小后的图片的base64编码字符串;err是错误信息,如果执行过程中发生错误则不为nil。 40 | func ResizeMultipartImgToBase64(f *multipart.FileHeader, width, height int) (base64Str string, err error) { 41 | file, err := f.Open() 42 | if err != nil { 43 | return 44 | } 45 | defer file.Close() 46 | 47 | return resizeImg2Base64(file, width, height) 48 | } 49 | 50 | // ResizeURLImgToBase64 从指定的URL获取图像,并将其调整为指定的宽度和高度后,转换为Base64编码的字符串。 51 | // 参数: 52 | // 53 | // url - 图像的URL地址。 54 | // width - 调整后图像的宽度。 55 | // height - 调整后图像的高度。 56 | // 57 | // 返回值: 58 | // 59 | // base64Str - 转换后的Base64编码字符串。 60 | // err - 错误信息,如果执行过程中遇到任何错误,则返回该错误。 61 | func ResizeURLImgToBase64(url string, width, height int) (base64Str string, err error) { 62 | client := &http.Client{ 63 | Transport: &http.Transport{ 64 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 65 | }, 66 | } 67 | 68 | resp, err := client.Get(url) 69 | if err != nil { 70 | return 71 | } 72 | defer resp.Body.Close() 73 | 74 | return resizeImg2Base64(resp.Body, width, height) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/tools/useragent.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/02/08 14:43 4 | */ 5 | 6 | package tools 7 | 8 | import ( 9 | "fmt" 10 | "math/rand" 11 | ) 12 | 13 | var uaGens = []func() string{ 14 | genFirefoxUA, 15 | genChromeUA, 16 | } 17 | 18 | // RandomUserAgent generates a random browser user agent on every request 19 | func RandomUserAgent() string { 20 | return uaGens[rand.Intn(len(uaGens))]() 21 | } 22 | 23 | var ffVersions = []float32{ 24 | 58.0, 25 | 57.0, 26 | 56.0, 27 | 52.0, 28 | 48.0, 29 | 40.0, 30 | 35.0, 31 | } 32 | 33 | var chromeVersions = []string{ 34 | "65.0.3325.146", 35 | "64.0.3282.0", 36 | "41.0.2228.0", 37 | "40.0.2214.93", 38 | "37.0.2062.124", 39 | } 40 | 41 | var osStrings = []string{ 42 | "Macintosh; Intel Mac OS X 10_10", 43 | "Windows NT 10.0", 44 | "Windows NT 5.1", 45 | "Windows NT 6.1; WOW64", 46 | "Windows NT 6.1; Win64; x64", 47 | "X11; Linux x86_64", 48 | } 49 | 50 | func genFirefoxUA() string { 51 | version := ffVersions[rand.Intn(len(ffVersions))] 52 | os := osStrings[rand.Intn(len(osStrings))] 53 | return fmt.Sprintf("Mozilla/5.0 (%s; rv:%.1f) Gecko/20100101 Firefox/%.1f", os, version, version) 54 | } 55 | 56 | func genChromeUA() string { 57 | version := chromeVersions[rand.Intn(len(chromeVersions))] 58 | os := osStrings[rand.Intn(len(osStrings))] 59 | return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", os, version) 60 | } 61 | -------------------------------------------------------------------------------- /pkg/tools/work.go: -------------------------------------------------------------------------------- 1 | /** 2 | * @Author: chentong 3 | * @Date: 2025/01/06 12:30 4 | */ 5 | 6 | package tools 7 | 8 | import ( 9 | "sync" 10 | ) 11 | 12 | // WorkerPool 工作池结构 13 | type WorkerPool struct { 14 | workerNum int // 工作池中的 Worker 数量 15 | jobsChan chan func() // 任务通道,接收需要执行的任务 16 | wg sync.WaitGroup // 用于等待所有 Worker 完成 17 | } 18 | 19 | // NewWorkerPool 创建一个新的工作池 20 | func NewWorkerPool(workerNum, jobQueueSize int) *WorkerPool { 21 | return &WorkerPool{ 22 | workerNum: workerNum, 23 | jobsChan: make(chan func(), jobQueueSize), 24 | } 25 | } 26 | 27 | // Start 启动工作池 28 | func (wp *WorkerPool) Start() { 29 | for i := 0; i < wp.workerNum; i++ { 30 | wp.wg.Add(1) 31 | go func(workerID int) { 32 | defer wp.wg.Done() 33 | for job := range wp.jobsChan { 34 | job() // 执行任务 35 | } 36 | }(i + 1) 37 | } 38 | } 39 | 40 | // AddJob 向工作池中添加任务 41 | func (wp *WorkerPool) AddJob(job func()) { 42 | wp.jobsChan <- job 43 | } 44 | 45 | // Wait 等待所有任务完成并关闭工作池 46 | func (wp *WorkerPool) Wait() { 47 | close(wp.jobsChan) // 关闭任务通道,表示没有更多任务 48 | wp.wg.Wait() // 等待所有 Worker 完成 49 | } 50 | -------------------------------------------------------------------------------- /storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/storage/.gitkeep -------------------------------------------------------------------------------- /web/assets.go: -------------------------------------------------------------------------------- 1 | package assets 2 | 3 | import "embed" 4 | 5 | var ( 6 | //go:embed static 7 | Static embed.FS 8 | 9 | //go:embed templates 10 | Templates embed.FS 11 | ) 12 | -------------------------------------------------------------------------------- /web/static/admin/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/web/static/admin/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /web/static/admin/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/web/static/admin/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /web/static/admin/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/web/static/admin/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /web/static/admin/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/web/static/admin/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /web/static/admin/image/gr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/web/static/admin/image/gr-code.png -------------------------------------------------------------------------------- /web/static/admin/js/authorization/enc-base64.min.js: -------------------------------------------------------------------------------- 1 | (function(h,s){typeof exports=="object"?module.exports=exports=s(require("./core")):typeof define=="function"&&define.amd?define(["./core"],s):s(h.CryptoJS)})(this,function(h){var s;return s=h.lib.WordArray,h.enc.Base64={stringify:function(t){var f=t.words,n=t.sigBytes,a=this._map;t.clamp();for(var r=[],e=0;e>>2]>>>24-e%4*8&255)<<16|(f[e+1>>>2]>>>24-(e+1)%4*8&255)<<8|f[e+2>>>2]>>>24-(e+2)%4*8&255,i=0;i<4&&e+.75*i>>6*(3-i)&63));var o=a.charAt(64);if(o)for(;r.length%4;)r.push(o);return r.join("")},parse:function(t){var f=t.length,n=this._map,a=this._reverseMap;if(!a){a=this._reverseMap=[];for(var r=0;r>>6-c%4*2,m=A|l;d[u>>>2]|=m<<24-u%4*8,u++}return s.create(d,u)}(t,f,a)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="},h.enc.Base64}); 2 | -------------------------------------------------------------------------------- /web/static/admin/js/authorization/hmac-sha256.js: -------------------------------------------------------------------------------- 1 | (function(e,i,r){typeof exports=="object"?module.exports=exports=i(require("./core"),require("./sha256"),require("./hmac")):typeof define=="function"&&define.amd?define(["./core","./sha256","./hmac"],i):i(e.CryptoJS)})(this,function(e){return e.HmacSHA256}); 2 | -------------------------------------------------------------------------------- /web/static/admin/js/authorization/ksort.js: -------------------------------------------------------------------------------- 1 | function ksort(e,u){var n={},h=[],a,t,i,j=this,o=!1,p={};switch(u){case"SORT_STRING":a=function(s,r){return j.strnatcmp(s,r)};break;case"SORT_LOCALE_STRING":var F=this.i18n_loc_get_default();a=this.php_js.i18nLocales[F].sorting;break;case"SORT_NUMERIC":a=function(s,r){return s+0-(r+0)};break;default:a=function(s,r){var l=parseFloat(s),c=parseFloat(r),f=l+""===s,_=c+""===r;return f&&_?l>c?1:lr?1:s<\/script>'),document.write(' 53 | ... 54 | 62 | 63 | 64 | 65 | 66 | 67 |
TypeValueDate
68 | ``` 69 | 70 | ## Options 71 | 72 | ### usePipeline 73 | 74 | * type: Boolean 75 | * description: Set true to enable pipelining 76 | * default: `false` 77 | 78 | ## pipelineSize 79 | 80 | * type: Integer 81 | * description: Size of each cache window. Must be greater than 0 82 | * default: `1000` 83 | 84 | ## Events 85 | 86 | ### onCachedDataHit(cached-data-hit.bs.table) 87 | 88 | * Fires when paging was able to use the locally cached data. 89 | 90 | ### onCachedDataReset(cached-data-reset.bs.table) 91 | 92 | * Fires when the locally cached data needed to be reset (i.e. on sorting, searching, page size change or paged out of current cache window) 93 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/pipeline/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pipeline", 3 | "version": "1.0.0", 4 | "description": "Plugin to support a hybrid approach to server/client side paging.", 5 | "url": "", 6 | "example": "#", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-pipeline", 10 | "url": "" 11 | }], 12 | 13 | "author": { 14 | "name": "doug-the-guy", 15 | "image": "" 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/print/bootstrap-table-print.js: -------------------------------------------------------------------------------- 1 | const Utils=$.fn.bootstrapTable.utils;function printPageBuilderDefault(s){return` 2 | 3 | 4 | 10 | 35 | 36 | Print Table 37 | 38 |

Printed on: ${new Date}

39 |
${s}
40 | 41 | `}$.extend($.fn.bootstrapTable.defaults,{showPrint:!1,printAsFilteredAndSortedOnUI:!0,printSortColumn:void 0,printSortOrder:"asc",printPageBuilder(s){return printPageBuilderDefault(s)}}),$.extend($.fn.bootstrapTable.COLUMN_DEFAULTS,{printFilter:void 0,printIgnore:!1,printFormatter:void 0}),$.extend($.fn.bootstrapTable.defaults.icons,{print:{bootstrap3:"glyphicon-print icon-share"}[$.fn.bootstrapTable.theme]||"fa-print"}),$.BootstrapTable=class extends $.BootstrapTable{initToolbar(...s){if(this.showToolbar=this.showToolbar||this.options.showPrint,super.initToolbar(...s),!this.options.showPrint)return;const d=this.$toolbar.find(">.columns");let l=d.find("button.bs-print");l.length||(l=$(` 42 | `).appendTo(d)),l.off("click").on("click",()=>{this.doPrint(this.options.printAsFilteredAndSortedOnUI?this.getData():this.options.data.slice(0))})}doPrint(s){const d=(e,t,i)=>{const n=Utils.calculateObjectValue(i,i.printFormatter,[e[i.field],e,t],e[i.field]);return typeof n>"u"||n===null?this.options.undefinedText:n},l=(e,t)=>{const n=[``];for(const r of t){n.push("");for(let o=0;o${r[o].title}`);n.push("")}n.push("");for(let r=0;r");for(const o of t)for(let a=0;a",d(e[r],r,o[a]),"");n.push("")}return n.push("
"),n.join("")},h=(e,t,i)=>{if(!t)return e;let n=i!=="asc";return n=-(+n||-1),e.sort((r,o)=>n*r[t].localeCompare(o[t]))},u=(e,t)=>{for(let i=0;ie.filter(i=>u(i,t)))(s,(e=>!e||!e[0]?[]:e[0].filter(t=>t.printFilter).map(t=>({colName:t.field,value:t.printFilter})))(this.options.columns)),s=h(s,this.options.printSortColumn,this.options.printSortOrder);const f=l(s,this.options.columns),p=window.open("");p.document.write(this.options.printPageBuilder.call(this,f)),p.document.close(),p.focus(),p.print(),p.close()}}; 48 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/reorder-columns/bootstrap-table-reorder-columns.js: -------------------------------------------------------------------------------- 1 | $.akottr.dragtable.prototype._restoreState=function(e){for(const[n,t]of Object.entries(e)){var o=this.originalTable.el.find(`th[data-field="${n}"]`);this.originalTable.startIndex=o.prevAll().length+1,this.originalTable.endIndex=parseInt(t,10)+1,this._bubbleCols()}};const filterFn=()=>{Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;const o=Object(this),n=o.length>>>0;if(typeof e!="function")throw new TypeError;const t=[],l=arguments.length>=2?arguments[1]:void 0;for(let s=0;s{const n={};o.el.find("th").each((r,d)=>{n[$(d).data("field")]=r}),this.columnsSortOrder=n,this.options.cookie&&this.persistReorderColumnsState(this);const t=[],l=[],s=[];let i=[],a=-1;const u=[];if(this.$header.find("th:not(.detail)").each(function(r){t.push($(this).data("field")),l.push($(this).data("formatter"))}),t.length!r.visible);for(var h=0;h{let f=!1;const m=d.field;this.options.columns[0].filter(c=>!f&&c.field===m?(u.push(c),f=!0,!1):!0)}),this.options.columns[0]=u,this.header.fields=t,this.header.formatters=l,this.initHeader(),this.initToolbar(),this.initSearchText(),this.initBody(),this.resetView(),this.trigger("reorder-column",t)}})}orderColumns(e){this.columnsSortOrder=e,this.makeRowsReorderable()}}; 2 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/reorder-columns/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Reorder Columns", 3 | "version": "1.1.0", 4 | "description": "Plugin to support the reordering columns feature.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/reorder-columns.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-reorder-columns", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/reorder-columns" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/reorder-rows/bootstrap-table-reorder-rows.js: -------------------------------------------------------------------------------- 1 | const rowAttr=(o,t)=>({id:`customId_${t}`});$.extend($.fn.bootstrapTable.defaults,{reorderableRows:!1,onDragStyle:null,onDropStyle:null,onDragClass:"reorder_rows_onDragClass",dragHandle:">tbody>tr>td",useRowAttrFunc:!1,onReorderRowsDrag(o){return!1},onReorderRowsDrop(o){return!1},onReorderRow(o){return!1}}),$.extend($.fn.bootstrapTable.Constructor.EVENTS,{"reorder-row.bs.table":"onReorderRow"}),$.BootstrapTable=class extends $.BootstrapTable{init(...o){if(!this.options.reorderableRows){super.init(...o);return}this.options.useRowAttrFunc&&(this.options.rowAttributes=rowAttr);const t=this.options.onPostBody;this.options.onPostBody=()=>{setTimeout(()=>{this.makeRowsReorderable(),t.call(this.options,this.options.data)},1)},super.init(...o)}makeRowsReorderable(){this.$el.tableDnD({onDragStyle:this.options.onDragStyle,onDropStyle:this.options.onDropStyle,onDragClass:this.options.onDragClass,onDragStart:(o,t)=>this.onDropStart(o,t),onDrop:(o,t)=>this.onDrop(o,t),dragHandle:this.options.dragHandle})}onDropStart(o,t){this.$draggingTd=$(t).css("cursor","move"),this.draggingIndex=$(this.$draggingTd.parent()).data("index"),this.options.onReorderRowsDrag(this.data[this.draggingIndex])}onDrop(o){this.$draggingTd.css("cursor","");const t=[];for(let s=0;se.$el.data("resizableColumns")!==void 0,initResizable=e=>{e.options.resizable&&!e.options.cardView&&!isInit(e)&&e.$el.resizableColumns({store:window.store})},destroy=e=>{isInit(e)&&e.$el.data("resizableColumns").destroy()},reInitResizable=e=>{destroy(e),initResizable(e)};$.extend($.fn.bootstrapTable.defaults,{resizable:!1});const BootstrapTable=$.fn.bootstrapTable.Constructor,_initBody=BootstrapTable.prototype.initBody,_toggleView=BootstrapTable.prototype.toggleView,_resetView=BootstrapTable.prototype.resetView;BootstrapTable.prototype.initBody=function(...e){const t=this;_initBody.apply(this,Array.prototype.slice.apply(e)),t.$el.off("column-switch.bs.table page-change.bs.table").on("column-switch.bs.table page-change.bs.table",()=>{reInitResizable(t)})},BootstrapTable.prototype.toggleView=function(...e){_toggleView.apply(this,Array.prototype.slice.apply(e)),this.options.resizable&&this.options.cardView&&destroy(this)},BootstrapTable.prototype.resetView=function(...e){const t=this;_resetView.apply(this,Array.prototype.slice.apply(e)),this.options.resizable&&setTimeout(()=>{initResizable(t)},100)}; 2 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/resizable/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Resizable", 3 | "version": "1.1.0", 4 | "description": "Plugin to support the resizable feature.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/resizable.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-resizable", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/resizable" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.js: -------------------------------------------------------------------------------- 1 | const Utils=$.fn.bootstrapTable.utils;$.extend($.fn.bootstrapTable.defaults,{stickyHeader:!1,stickyHeaderOffsetY:0,stickyHeaderOffsetLeft:0,stickyHeaderOffsetRight:0}),$.BootstrapTable=class extends $.BootstrapTable{initHeader(...t){super.initHeader(...t),this.options.stickyHeader&&(this.$tableBody.find(".sticky-header-container,.sticky_anchor_begin,.sticky_anchor_end").remove(),this.$el.before('
'),this.$el.before('
'),this.$el.after('
'),this.$header.addClass("sticky-header"),this.$stickyContainer=this.$tableBody.find(".sticky-header-container"),this.$stickyBegin=this.$tableBody.find(".sticky_anchor_begin"),this.$stickyEnd=this.$tableBody.find(".sticky_anchor_end"),this.$stickyHeader=this.$header.clone(!0,!0),$(window).off("resize.sticky-header-table").on("resize.sticky-header-table",()=>this.renderStickyHeader()),$(window).off("scroll.sticky-header-table").on("scroll.sticky-header-table",()=>this.renderStickyHeader()),this.$tableBody.off("scroll").on("scroll",()=>this.matchPositionX()))}onColumnSearch({currentTarget:t,keyCode:s}){super.onColumnSearch({currentTarget:t,keyCode:s}),this.renderStickyHeader()}resetView(...t){super.resetView(...t),$(".bootstrap-table.fullscreen").off("scroll").on("scroll",()=>this.renderStickyHeader())}renderStickyHeader(){const t=this;this.$stickyHeader=this.$header.clone(!0,!0),this.options.filterControl&&$(this.$stickyHeader).off("keyup change mouseup").on("keyup change mouse",function(i){const e=$(i.target),a=e.val(),o=e.parents("th").data("field"),r=t.$header.find('th[data-field="'+o+'"]');if(e.is("input"))r.find("input").val(a);else if(e.is("select")){const n=r.find("select");n.find("option[selected]").removeAttr("selected"),n.find('option[value="'+a+'"]').attr("selected",!0)}t.triggerSearch()});const s=$(window).scrollTop(),c=this.$stickyBegin.offset().top-this.options.stickyHeaderOffsetY,d=this.$stickyEnd.offset().top-this.options.stickyHeaderOffsetY-this.$header.height();if(s>c&&s<=d){this.$stickyHeader.find("tr:eq(0)").find("th").each((a,o)=>{$(o).css("min-width",this.$header.find("tr:eq(0)").find("th").eq(a).css("width"))}),this.$stickyContainer.show().addClass("fix-sticky fixed-table-container");let i=this.options.stickyHeaderOffsetLeft,e=this.options.stickyHeaderOffsetRight;this.$el.closest(".bootstrap-table").hasClass("fullscreen")&&(i=0,e=0),this.$stickyContainer.css("top",`${this.options.stickyHeaderOffsetY}`),this.$stickyContainer.css("left",`${i}`),this.$stickyContainer.css("right",`${e}`),this.$stickyTable=$(""),this.$stickyTable.addClass(this.options.classes),this.$stickyContainer.html(this.$stickyTable.append(this.$stickyHeader)),this.matchPositionX()}else this.$stickyContainer.removeClass("fix-sticky").hide()}matchPositionX(){this.$stickyContainer.scrollLeft(this.$tableBody.scrollLeft())}}; 2 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/sticky-header/bootstrap-table-sticky-header.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * @author vincent loh 3 | * @update zhixin wen 4 | */ 5 | 6 | .fix-sticky { 7 | position: fixed !important; 8 | overflow: hidden; 9 | z-index: 100; 10 | } 11 | 12 | .fix-sticky table thead { 13 | background: #fff; 14 | } 15 | 16 | .fix-sticky table thead.thead-light { 17 | background: #e9ecef; 18 | } 19 | 20 | .fix-sticky table thead.thead-dark { 21 | background: #212529; 22 | } 23 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/sticky-header/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sticky Header", 3 | "version": "1.0.0", 4 | "description": "An extension which provides a sticky header for table columns when scrolling on a long page and / or table. Works for tables with many columns and narrow width with horizontal scrollbars too.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/sticky-header.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-sticky-header", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/sticky-header" 11 | }], 12 | 13 | "author": { 14 | "name": "vinzloh", 15 | "image": "https://avatars0.githubusercontent.com/u/5501845" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/static/plugin/bootstrap-table/extensions/toolbar/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Toolbar", 3 | "version": "2.0.0", 4 | "description": "Plugin to support the advanced search.", 5 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar", 6 | "example": "http://issues.wenzhixin.net.cn/bootstrap-table/#extensions/toolbar.html", 7 | 8 | "plugins": [{ 9 | "name": "bootstrap-table-toolbar", 10 | "url": "https://github.com/wenzhixin/bootstrap-table/tree/master/src/extensions/toolbar" 11 | }], 12 | 13 | "author": { 14 | "name": "djhvscf", 15 | "image": "https://avatars1.githubusercontent.com/u/4496763" 16 | } 17 | } -------------------------------------------------------------------------------- /web/static/plugin/jquery-treegrid/jquery.treegrid.min.css: -------------------------------------------------------------------------------- 1 | .treegrid-indent{width:16px;height:16px;display:inline-block;position:relative}.treegrid-expander{width:16px;height:16px;display:inline-block;position:relative;cursor:pointer} 2 | -------------------------------------------------------------------------------- /web/upload/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ch3nnn/webstack-go/8ef443620e3e9c9796fb1c4c81d5a0de29a0f534/web/upload/favicon.png --------------------------------------------------------------------------------