Compare commits
No commits in common. "7d46b31866bb676bb0073c717ede213a8bb511a4" and "ea94f3c9f97f2f7d6a9ca2691966de3a49d3a8ea" have entirely different histories.
7d46b31866
...
ea94f3c9f9
27
.github/environments/production.yml
vendored
27
.github/environments/production.yml
vendored
@ -1,27 +0,0 @@
|
|||||||
# Production Environment Configuration for GitHub Actions
|
|
||||||
# This file defines production-specific deployment settings
|
|
||||||
|
|
||||||
environment:
|
|
||||||
name: production
|
|
||||||
url: https://api.ppanel.example.com
|
|
||||||
protection_rules:
|
|
||||||
- type: wait_timer
|
|
||||||
minutes: 5
|
|
||||||
- type: reviewers
|
|
||||||
reviewers:
|
|
||||||
- "@admin-team"
|
|
||||||
- "@devops-team"
|
|
||||||
variables:
|
|
||||||
ENVIRONMENT: production
|
|
||||||
LOG_LEVEL: info
|
|
||||||
DEPLOY_TIMEOUT: 300
|
|
||||||
|
|
||||||
# Environment-specific secrets required:
|
|
||||||
# PRODUCTION_HOST - Production server hostname/IP
|
|
||||||
# PRODUCTION_USER - SSH username for production server
|
|
||||||
# PRODUCTION_SSH_KEY - SSH private key for production server
|
|
||||||
# PRODUCTION_PORT - SSH port (default: 22)
|
|
||||||
# PRODUCTION_URL - Application URL for health checks
|
|
||||||
# DATABASE_PASSWORD - Production database password
|
|
||||||
# REDIS_PASSWORD - Production Redis password
|
|
||||||
# JWT_SECRET - JWT secret key for production
|
|
||||||
23
.github/environments/staging.yml
vendored
23
.github/environments/staging.yml
vendored
@ -1,23 +0,0 @@
|
|||||||
# Staging Environment Configuration for GitHub Actions
|
|
||||||
# This file defines staging-specific deployment settings
|
|
||||||
|
|
||||||
environment:
|
|
||||||
name: staging
|
|
||||||
url: https://staging-api.ppanel.example.com
|
|
||||||
protection_rules:
|
|
||||||
- type: wait_timer
|
|
||||||
minutes: 2
|
|
||||||
variables:
|
|
||||||
ENVIRONMENT: staging
|
|
||||||
LOG_LEVEL: debug
|
|
||||||
DEPLOY_TIMEOUT: 180
|
|
||||||
|
|
||||||
# Environment-specific secrets required:
|
|
||||||
# STAGING_HOST - Staging server hostname/IP
|
|
||||||
# STAGING_USER - SSH username for staging server
|
|
||||||
# STAGING_SSH_KEY - SSH private key for staging server
|
|
||||||
# STAGING_PORT - SSH port (default: 22)
|
|
||||||
# STAGING_URL - Application URL for health checks
|
|
||||||
# DATABASE_PASSWORD - Staging database password
|
|
||||||
# REDIS_PASSWORD - Staging Redis password
|
|
||||||
# JWT_SECRET - JWT secret key for staging
|
|
||||||
79
.github/workflows/deploy-linux.yml
vendored
79
.github/workflows/deploy-linux.yml
vendored
@ -1,79 +0,0 @@
|
|||||||
name: Build Linux Binary
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main, master ]
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Version to build (leave empty for auto)'
|
|
||||||
required: false
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Linux Binary
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.23.3'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
GOOS: linux
|
|
||||||
GOARCH: amd64
|
|
||||||
run: |
|
|
||||||
VERSION=${{ github.event.inputs.version }}
|
|
||||||
if [ -z "$VERSION" ]; then
|
|
||||||
VERSION=$(git describe --tags --always --dirty)
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Building ppanel-server $VERSION"
|
|
||||||
BUILD_TIME=$(date +"%Y-%m-%d_%H:%M:%S")
|
|
||||||
go build -ldflags="-w -s -X github.com/perfect-panel/server/pkg/constant.Version=$VERSION -X github.com/perfect-panel/server/pkg/constant.BuildTime=$BUILD_TIME" -o ppanel-server ./ppanel.go
|
|
||||||
tar -czf ppanel-server-${VERSION}-linux-amd64.tar.gz ppanel-server
|
|
||||||
sha256sum ppanel-server ppanel-server-${VERSION}-linux-amd64.tar.gz > checksum.txt
|
|
||||||
|
|
||||||
- name: Upload artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ppanel-server-linux-amd64
|
|
||||||
path: |
|
|
||||||
ppanel-server
|
|
||||||
ppanel-server-*-linux-amd64.tar.gz
|
|
||||||
checksum.txt
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
VERSION=${GITHUB_REF#refs/tags/}
|
|
||||||
|
|
||||||
# Check if release exists
|
|
||||||
if gh release view $VERSION >/dev/null 2>&1; then
|
|
||||||
echo "Release $VERSION already exists, deleting old assets..."
|
|
||||||
# Delete existing assets if they exist
|
|
||||||
gh release delete-asset $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz --yes 2>/dev/null || true
|
|
||||||
gh release delete-asset $VERSION checksum.txt --yes 2>/dev/null || true
|
|
||||||
else
|
|
||||||
echo "Creating new release $VERSION..."
|
|
||||||
gh release create $VERSION --title "PPanel Server $VERSION" --notes "Release $VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Upload assets (will overwrite if --clobber is supported, otherwise will fail gracefully)
|
|
||||||
echo "Uploading assets..."
|
|
||||||
gh release upload $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz checksum.txt --clobber
|
|
||||||
51
.github/workflows/develop.yaml
vendored
Normal file
51
.github/workflows/develop.yaml
vendored
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["develop"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["develop"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Get short Git commit ID
|
||||||
|
id: vars
|
||||||
|
run: echo "COMMIT_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build --build-arg VERSION=${{ env.COMMIT_ID }} -t ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} .
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
run: docker push ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }}
|
||||||
|
|
||||||
|
# - name: Deploy to server
|
||||||
|
# uses: appleboy/ssh-action@v0.1.6
|
||||||
|
# with:
|
||||||
|
# host: ${{ secrets.SSH_HOST }}
|
||||||
|
# username: ${{ secrets.SSH_USER }}
|
||||||
|
# key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||||
|
# script: |
|
||||||
|
# if [ $(docker ps -a -q -f name=ppanel-server-dev) ]; then
|
||||||
|
# echo "Stopping and removing existing ppanel-server container..."
|
||||||
|
# docker stop ppanel-server-dev
|
||||||
|
# docker rm ppanel-server-dev
|
||||||
|
# else
|
||||||
|
# echo "No existing ppanel-server-dev container running."
|
||||||
|
# fi
|
||||||
|
#
|
||||||
|
# docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
# docker run -d --restart=always --log-driver=journald --name ppanel-server-dev -p 8080:8080 -v /www/wwwroot/api/etc:/app/etc -v /www/wwwroot/api/logs:/app/logs --restart=always -d ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }}
|
||||||
|
#
|
||||||
131
.github/workflows/release.yml
vendored
Normal file
131
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
name: Release
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: ppanel-server
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract version from git tag
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Get short SHA
|
||||||
|
id: sha
|
||||||
|
run: echo "GIT_SHA=${GITHUB_SHA::8}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set BUILD_TIME env
|
||||||
|
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build and push Docker image for main release
|
||||||
|
if: "!contains(github.ref_name, 'beta')"
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ env.VERSION }}
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image for beta release
|
||||||
|
if: contains(github.ref_name, 'beta')
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ env.VERSION }}
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:beta
|
||||||
|
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
|
||||||
|
|
||||||
|
release-notes:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build-docker
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Install GoReleaser
|
||||||
|
run: |
|
||||||
|
go install github.com/goreleaser/goreleaser/v2@latest
|
||||||
|
|
||||||
|
- name: Run GoReleaser
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
goreleaser check
|
||||||
|
goreleaser release --clean
|
||||||
|
|
||||||
|
releases-matrix:
|
||||||
|
name: Release ppanel-server binary
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release-notes # wait for release-notes job to finish
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
|
||||||
|
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
|
||||||
|
goos: [ linux, windows, darwin ]
|
||||||
|
goarch: [ '386', amd64, arm64 ]
|
||||||
|
exclude:
|
||||||
|
- goarch: '386'
|
||||||
|
goos: darwin
|
||||||
|
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Extract version from git tag
|
||||||
|
id: version
|
||||||
|
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set BUILD_TIME env
|
||||||
|
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: wangyoucao577/go-release-action@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
goos: ${{ matrix.goos }}
|
||||||
|
goarch: ${{ matrix.goarch }}
|
||||||
|
asset_name: "ppanel-server-${{ matrix.goos }}-${{ matrix.goarch }}"
|
||||||
|
goversion: "https://dl.google.com/go/go1.23.3.linux-amd64.tar.gz"
|
||||||
|
project_path: "."
|
||||||
|
binary_name: "ppanel-server"
|
||||||
|
extra_files: LICENSE etc
|
||||||
|
ldflags: -X "github.com/perfect-panel/server/pkg/constant.Version=${{env.VERSION}}" -X "github.com/perfect-panel/server/pkg/constant.BuildTime=${{env.BUILD_TIME}}"
|
||||||
81
.github/workflows/swagger.yaml
vendored
Normal file
81
.github/workflows/swagger.yaml
vendored
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
name: Go CI/CD with goctl and Swagger
|
||||||
|
|
||||||
|
on:
|
||||||
|
# release:
|
||||||
|
# types: [published]
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install goctl
|
||||||
|
run: |
|
||||||
|
curl -L https://github.com/zeromicro/go-zero/releases/download/tools%2Fgoctl%2Fv1.7.2/goctl-v1.7.2-linux-amd64.tar.gz -o goctl-v1.7.2-linux-amd64.tar.gz
|
||||||
|
tar -xvzf goctl-v1.7.2-linux-amd64.tar.gz
|
||||||
|
chmod +x goctl
|
||||||
|
sudo mv goctl /usr/local/bin/goctl
|
||||||
|
goctl --version
|
||||||
|
|
||||||
|
- name: Install goctl-swagger
|
||||||
|
run: |
|
||||||
|
curl -L https://github.com/tensionc/goctl-swagger/releases/download/v1.0.1/goctl-swagger-v1.0.1-linux-amd64.tar.gz -o goctl-swagger.tar.gz
|
||||||
|
tar -xvzf goctl-swagger.tar.gz
|
||||||
|
chmod +x goctl-swagger
|
||||||
|
sudo mv goctl-swagger /usr/local/bin/
|
||||||
|
|
||||||
|
- name: Generate Swagger file
|
||||||
|
run: |
|
||||||
|
mkdir -p swagger
|
||||||
|
goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_common.api -dir ./swagger
|
||||||
|
goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_user.api -dir ./swagger
|
||||||
|
goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_admin.api -dir ./swagger
|
||||||
|
goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir ./swagger
|
||||||
|
goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_node.api -dir ./swagger
|
||||||
|
|
||||||
|
|
||||||
|
- name: Verify Swagger file
|
||||||
|
run: |
|
||||||
|
test -f ./swagger/common.json
|
||||||
|
test -f ./swagger/user.json
|
||||||
|
test -f ./swagger/admin.json
|
||||||
|
|
||||||
|
- name: Checkout target repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: perfect-panel/ppanel-docs
|
||||||
|
token: ${{ secrets.GH_TOKEN }}
|
||||||
|
path: ppanel-docs
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Verify or create public/swagger directory
|
||||||
|
run: |
|
||||||
|
mkdir -p ./ppanel-docs/public/swagger
|
||||||
|
|
||||||
|
- name: Copy Swagger files
|
||||||
|
run: |
|
||||||
|
cp -rf swagger/* ppanel-docs/public/swagger
|
||||||
|
cd ppanel-docs
|
||||||
|
|
||||||
|
- name: Check for file changes
|
||||||
|
run: |
|
||||||
|
cd ppanel-docs
|
||||||
|
git add .
|
||||||
|
git status
|
||||||
|
if [ "$(git status --porcelain)" ]; then
|
||||||
|
echo "Changes detected in the doc repository."
|
||||||
|
git config user.name "GitHub Actions"
|
||||||
|
git config user.email "actions@ppanel.dev"
|
||||||
|
git commit -m "Update Swagger files"
|
||||||
|
git push
|
||||||
|
else
|
||||||
|
echo "No changes detected."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
130
.goreleaser.yml
130
.goreleaser.yml
@ -1,130 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
before:
|
|
||||||
hooks:
|
|
||||||
- go mod tidy
|
|
||||||
- go generate ./...
|
|
||||||
|
|
||||||
builds:
|
|
||||||
- env:
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
goos:
|
|
||||||
- linux
|
|
||||||
- darwin
|
|
||||||
- windows
|
|
||||||
goarch:
|
|
||||||
- "386"
|
|
||||||
- amd64
|
|
||||||
- arm64
|
|
||||||
ignore:
|
|
||||||
- goos: darwin
|
|
||||||
goarch: "386"
|
|
||||||
binary: ppanel-server
|
|
||||||
ldflags:
|
|
||||||
- -s -w
|
|
||||||
- -X "github.com/perfect-panel/server/pkg/constant.Version={{.Version}}"
|
|
||||||
- -X "github.com/perfect-panel/server/pkg/constant.BuildTime={{.Date}}"
|
|
||||||
- -X "github.com/perfect-panel/server/pkg/constant.GitCommit={{.Commit}}"
|
|
||||||
main: ./ppanel.go
|
|
||||||
|
|
||||||
archives:
|
|
||||||
- format: tar.gz
|
|
||||||
name_template: >-
|
|
||||||
{{ .ProjectName }}-
|
|
||||||
{{- .Version }}-
|
|
||||||
{{- title .Os }}-
|
|
||||||
{{- if eq .Arch "amd64" }}x86_64
|
|
||||||
{{- else if eq .Arch "386" }}i386
|
|
||||||
{{- else }}{{ .Arch }}{{ end }}
|
|
||||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
|
||||||
files:
|
|
||||||
- LICENSE
|
|
||||||
- etc/*
|
|
||||||
format_overrides:
|
|
||||||
- goos: windows
|
|
||||||
format: zip
|
|
||||||
|
|
||||||
checksum:
|
|
||||||
name_template: "checksums.txt"
|
|
||||||
|
|
||||||
snapshot:
|
|
||||||
name_template: "{{ incpatch .Version }}-next"
|
|
||||||
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
use: github
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- "^docs:"
|
|
||||||
- "^test:"
|
|
||||||
- "^chore:"
|
|
||||||
- Merge pull request
|
|
||||||
groups:
|
|
||||||
- title: Features
|
|
||||||
regexp: "^.*feat[(\\w)]*:+.*$"
|
|
||||||
order: 0
|
|
||||||
- title: 'Bug fixes'
|
|
||||||
regexp: "^.*fix[(\\w)]*:+.*$"
|
|
||||||
order: 1
|
|
||||||
- title: Others
|
|
||||||
order: 999
|
|
||||||
|
|
||||||
dockers:
|
|
||||||
- image_templates:
|
|
||||||
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}"
|
|
||||||
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}"
|
|
||||||
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}.{{ .Minor }}"
|
|
||||||
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:latest"
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--pull"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--platform=linux/amd64"
|
|
||||||
use: docker
|
|
||||||
extra_files:
|
|
||||||
- etc/
|
|
||||||
|
|
||||||
- image_templates:
|
|
||||||
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}-arm64"
|
|
||||||
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}.{{ .Minor }}-arm64"
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
build_flag_templates:
|
|
||||||
- "--pull"
|
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
|
||||||
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
|
||||||
- "--platform=linux/arm64"
|
|
||||||
use: docker
|
|
||||||
goarch: arm64
|
|
||||||
extra_files:
|
|
||||||
- etc/
|
|
||||||
|
|
||||||
docker_signs:
|
|
||||||
- cmd: cosign
|
|
||||||
args:
|
|
||||||
- "sign"
|
|
||||||
- "${artifact}@${digest}"
|
|
||||||
env:
|
|
||||||
- COSIGN_EXPERIMENTAL=1
|
|
||||||
|
|
||||||
release:
|
|
||||||
github:
|
|
||||||
owner: perfect-panel
|
|
||||||
name: server
|
|
||||||
draft: false
|
|
||||||
prerelease: auto
|
|
||||||
name_template: "{{.ProjectName}} v{{.Version}}"
|
|
||||||
header: |
|
|
||||||
## ppanel-server {{.Version}}
|
|
||||||
|
|
||||||
Welcome to this new release!
|
|
||||||
footer: |
|
|
||||||
Docker images are available at:
|
|
||||||
- `{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}`
|
|
||||||
- `{{ .Env.DOCKER_USERNAME }}/ppanel-server:latest`
|
|
||||||
|
|
||||||
For more information, visit our documentation.
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="go build github.com/perfect-panel/server" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
|
|
||||||
<module name="server" />
|
|
||||||
<working_directory value="$PROJECT_DIR$" />
|
|
||||||
<parameters value="run --config etc/ppanel-dev.yaml" />
|
|
||||||
<kind value="PACKAGE" />
|
|
||||||
<package value="github.com/perfect-panel/server" />
|
|
||||||
<directory value="$PROJECT_DIR$" />
|
|
||||||
<filePath value="$PROJECT_DIR$/ppanel.go" />
|
|
||||||
<method v="2" />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
syntax = "v1"
|
|
||||||
|
|
||||||
info (
|
|
||||||
title: "redemption API"
|
|
||||||
desc: "API for redemption code management"
|
|
||||||
author: "Tension"
|
|
||||||
email: "tension@ppanel.com"
|
|
||||||
version: "0.0.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
import "../types.api"
|
|
||||||
|
|
||||||
type (
|
|
||||||
CreateRedemptionCodeRequest {
|
|
||||||
TotalCount int64 `json:"total_count" validate:"required"`
|
|
||||||
SubscribePlan int64 `json:"subscribe_plan" validate:"required"`
|
|
||||||
UnitTime string `json:"unit_time" validate:"required,oneof=day month quarter half_year year"`
|
|
||||||
Quantity int64 `json:"quantity" validate:"required"`
|
|
||||||
BatchCount int64 `json:"batch_count" validate:"required,min=1"`
|
|
||||||
}
|
|
||||||
UpdateRedemptionCodeRequest {
|
|
||||||
Id int64 `json:"id" validate:"required"`
|
|
||||||
TotalCount int64 `json:"total_count,omitempty"`
|
|
||||||
SubscribePlan int64 `json:"subscribe_plan,omitempty"`
|
|
||||||
UnitTime string `json:"unit_time,omitempty" validate:"omitempty,oneof=day month quarter half_year year"`
|
|
||||||
Quantity int64 `json:"quantity,omitempty"`
|
|
||||||
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
|
|
||||||
}
|
|
||||||
ToggleRedemptionCodeStatusRequest {
|
|
||||||
Id int64 `json:"id" validate:"required"`
|
|
||||||
Status int64 `json:"status" validate:"oneof=0 1"`
|
|
||||||
}
|
|
||||||
DeleteRedemptionCodeRequest {
|
|
||||||
Id int64 `json:"id" validate:"required"`
|
|
||||||
}
|
|
||||||
BatchDeleteRedemptionCodeRequest {
|
|
||||||
Ids []int64 `json:"ids" validate:"required"`
|
|
||||||
}
|
|
||||||
GetRedemptionCodeListRequest {
|
|
||||||
Page int64 `form:"page" validate:"required"`
|
|
||||||
Size int64 `form:"size" validate:"required"`
|
|
||||||
SubscribePlan int64 `form:"subscribe_plan,omitempty"`
|
|
||||||
UnitTime string `form:"unit_time,omitempty"`
|
|
||||||
Code string `form:"code,omitempty"`
|
|
||||||
}
|
|
||||||
GetRedemptionCodeListResponse {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []RedemptionCode `json:"list"`
|
|
||||||
}
|
|
||||||
GetRedemptionRecordListRequest {
|
|
||||||
Page int64 `form:"page" validate:"required"`
|
|
||||||
Size int64 `form:"size" validate:"required"`
|
|
||||||
UserId int64 `form:"user_id,omitempty"`
|
|
||||||
CodeId int64 `form:"code_id,omitempty"`
|
|
||||||
}
|
|
||||||
GetRedemptionRecordListResponse {
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
List []RedemptionRecord `json:"list"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@server (
|
|
||||||
prefix: v1/admin/redemption
|
|
||||||
group: admin/redemption
|
|
||||||
middleware: AuthMiddleware
|
|
||||||
)
|
|
||||||
service ppanel {
|
|
||||||
@doc "Create redemption code"
|
|
||||||
@handler CreateRedemptionCode
|
|
||||||
post /code (CreateRedemptionCodeRequest)
|
|
||||||
|
|
||||||
@doc "Update redemption code"
|
|
||||||
@handler UpdateRedemptionCode
|
|
||||||
put /code (UpdateRedemptionCodeRequest)
|
|
||||||
|
|
||||||
@doc "Toggle redemption code status"
|
|
||||||
@handler ToggleRedemptionCodeStatus
|
|
||||||
put /code/status (ToggleRedemptionCodeStatusRequest)
|
|
||||||
|
|
||||||
@doc "Delete redemption code"
|
|
||||||
@handler DeleteRedemptionCode
|
|
||||||
delete /code (DeleteRedemptionCodeRequest)
|
|
||||||
|
|
||||||
@doc "Batch delete redemption code"
|
|
||||||
@handler BatchDeleteRedemptionCode
|
|
||||||
delete /code/batch (BatchDeleteRedemptionCodeRequest)
|
|
||||||
|
|
||||||
@doc "Get redemption code list"
|
|
||||||
@handler GetRedemptionCodeList
|
|
||||||
get /code/list (GetRedemptionCodeListRequest) returns (GetRedemptionCodeListResponse)
|
|
||||||
|
|
||||||
@doc "Get redemption record list"
|
|
||||||
@handler GetRedemptionRecordList
|
|
||||||
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
|
|
||||||
}
|
|
||||||
@ -22,7 +22,6 @@ type (
|
|||||||
Unscoped bool `form:"unscoped,omitempty"`
|
Unscoped bool `form:"unscoped,omitempty"`
|
||||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||||
ShortCode string `form:"short_code,omitempty"`
|
|
||||||
}
|
}
|
||||||
// GetUserListResponse
|
// GetUserListResponse
|
||||||
GetUserListResponse {
|
GetUserListResponse {
|
||||||
@ -180,7 +179,7 @@ type (
|
|||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
DeleteUserSubscribeRequest {
|
DeleteUserSubscribeRequest {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id,string"`
|
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||||
}
|
}
|
||||||
GetUserSubscribeByIdRequest {
|
GetUserSubscribeByIdRequest {
|
||||||
Id int64 `form:"id" validate:"required"`
|
Id int64 `form:"id" validate:"required"`
|
||||||
|
|||||||
@ -124,7 +124,6 @@ type (
|
|||||||
IP string `header:"X-Original-Forwarded-For"`
|
IP string `header:"X-Original-Forwarded-For"`
|
||||||
UserAgent string `json:"user_agent" validate:"required"`
|
UserAgent string `json:"user_agent" validate:"required"`
|
||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
ShortCode string `json:"short_code,optional"`
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
syntax = "v1"
|
|
||||||
|
|
||||||
info (
|
|
||||||
title: "redemption API"
|
|
||||||
desc: "API for redemption"
|
|
||||||
author: "Tension"
|
|
||||||
email: "tension@ppanel.com"
|
|
||||||
version: "0.0.1"
|
|
||||||
)
|
|
||||||
|
|
||||||
import "../types.api"
|
|
||||||
|
|
||||||
type (
|
|
||||||
RedeemCodeRequest {
|
|
||||||
Code string `json:"code" validate:"required"`
|
|
||||||
}
|
|
||||||
RedeemCodeResponse {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@server (
|
|
||||||
prefix: v1/public/redemption
|
|
||||||
group: public/redemption
|
|
||||||
jwt: JwtAuth
|
|
||||||
middleware: AuthMiddleware,DeviceMiddleware
|
|
||||||
)
|
|
||||||
service ppanel {
|
|
||||||
@doc "Redeem code"
|
|
||||||
@handler RedeemCode
|
|
||||||
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
|
|
||||||
}
|
|
||||||
@ -14,48 +14,40 @@ type (
|
|||||||
QuerySubscribeListRequest {
|
QuerySubscribeListRequest {
|
||||||
Language string `form:"language"`
|
Language string `form:"language"`
|
||||||
}
|
}
|
||||||
|
QueryUserSubscribeNodeListResponse {
|
||||||
QueryUserSubscribeNodeListResponse {
|
List []UserSubscribeInfo `json:"list"`
|
||||||
List []UserSubscribeInfo `json:"list"`
|
}
|
||||||
}
|
UserSubscribeInfo {
|
||||||
|
Id int64 `json:"id"`
|
||||||
UserSubscribeInfo {
|
UserId int64 `json:"user_id"`
|
||||||
Id int64 `json:"id"`
|
OrderId int64 `json:"order_id"`
|
||||||
UserId int64 `json:"user_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
OrderId int64 `json:"order_id"`
|
StartTime int64 `json:"start_time"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
ExpireTime int64 `json:"expire_time"`
|
||||||
StartTime int64 `json:"start_time"`
|
FinishedAt int64 `json:"finished_at"`
|
||||||
ExpireTime int64 `json:"expire_time"`
|
ResetTime int64 `json:"reset_time"`
|
||||||
FinishedAt int64 `json:"finished_at"`
|
Traffic int64 `json:"traffic"`
|
||||||
ResetTime int64 `json:"reset_time"`
|
Download int64 `json:"download"`
|
||||||
Traffic int64 `json:"traffic"`
|
Upload int64 `json:"upload"`
|
||||||
Download int64 `json:"download"`
|
Token string `json:"token"`
|
||||||
Upload int64 `json:"upload"`
|
Status uint8 `json:"status"`
|
||||||
Token string `json:"token"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
Status uint8 `json:"status"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
IsTryOut bool `json:"is_try_out"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||||
IsTryOut bool `json:"is_try_out"`
|
}
|
||||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
UserSubscribeNodeInfo {
|
||||||
}
|
Id int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
UserSubscribeNodeInfo{
|
Uuid string `json:"uuid"`
|
||||||
Id int64 `json:"id"`
|
Protocol string `json:"protocol"`
|
||||||
Name string `json:"name"`
|
Port uint16 `json:"port"`
|
||||||
Uuid string `json:"uuid"`
|
Address string `json:"address"`
|
||||||
Protocol string `json:"protocol"`
|
Tags []string `json:"tags"`
|
||||||
Protocols string `json:"protocols"`
|
Country string `json:"country"`
|
||||||
Port uint16 `json:"port"`
|
City string `json:"city"`
|
||||||
Address string `json:"address"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
Tags []string `json:"tags"`
|
}
|
||||||
Country string `json:"country"`
|
|
||||||
City string `json:"city"`
|
|
||||||
Longitude string `json:"longitude"`
|
|
||||||
Latitude string `json:"latitude"`
|
|
||||||
LatitudeCenter string `json:"latitude_center"`
|
|
||||||
LongitudeCenter string `json:"longitude_center"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -68,8 +60,8 @@ service ppanel {
|
|||||||
@handler QuerySubscribeList
|
@handler QuerySubscribeList
|
||||||
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
|
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
|
||||||
|
|
||||||
@doc "Get user subscribe node info"
|
@doc "Get user subscribe node info"
|
||||||
@handler QueryUserSubscribeNodeList
|
@handler QueryUserSubscribeNodeList
|
||||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,7 +66,6 @@ type (
|
|||||||
UnbindOAuthRequest {
|
UnbindOAuthRequest {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetLoginLogRequest {
|
GetLoginLogRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@ -95,21 +94,17 @@ type (
|
|||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
Code string `json:"code" validate:"required"`
|
Code string `json:"code" validate:"required"`
|
||||||
}
|
}
|
||||||
|
GetDeviceListResponse {
|
||||||
GetDeviceListResponse {
|
List []UserDevice `json:"list"`
|
||||||
List []UserDevice `json:"list"`
|
Total int64 `json:"total"`
|
||||||
Total int64 `json:"total"`
|
}
|
||||||
}
|
UnbindDeviceRequest {
|
||||||
|
Id int64 `json:"id" validate:"required"`
|
||||||
UnbindDeviceRequest {
|
}
|
||||||
Id int64 `json:"id" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateUserSubscribeNoteRequest {
|
UpdateUserSubscribeNoteRequest {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||||
Note string `json:"note" validate:"max=500"`
|
Note string `json:"note" validate:"max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateUserRulesRequest {
|
UpdateUserRulesRequest {
|
||||||
Rules []string `json:"rules" validate:"required"`
|
Rules []string `json:"rules" validate:"required"`
|
||||||
}
|
}
|
||||||
@ -135,23 +130,6 @@ type (
|
|||||||
List []WithdrawalLog `json:"list"`
|
List []WithdrawalLog `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
GetDeviceOnlineStatsResponse {
|
|
||||||
WeeklyStats []WeeklyStat `json:"weekly_stats"`
|
|
||||||
ConnectionRecords ConnectionRecords `json:"connection_records"`
|
|
||||||
}
|
|
||||||
|
|
||||||
WeeklyStat {
|
|
||||||
Day int `json:"day"`
|
|
||||||
DayName string `json:"day_name"`
|
|
||||||
Hours float64 `json:"hours"`
|
|
||||||
}
|
|
||||||
ConnectionRecords {
|
|
||||||
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
|
||||||
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
|
||||||
LongestSingleConnection int64 `json:"longest_single_connection"`
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -248,9 +226,9 @@ service ppanel {
|
|||||||
@handler UpdateBindEmail
|
@handler UpdateBindEmail
|
||||||
put /bind_email (UpdateBindEmailRequest)
|
put /bind_email (UpdateBindEmailRequest)
|
||||||
|
|
||||||
@doc "Get Device List"
|
@doc "Get Device List"
|
||||||
@handler GetDeviceList
|
@handler GetDeviceList
|
||||||
get /devices returns (GetDeviceListResponse)
|
get /devices returns (GetDeviceListResponse)
|
||||||
|
|
||||||
@doc "Unbind Device"
|
@doc "Unbind Device"
|
||||||
@handler UnbindDevice
|
@handler UnbindDevice
|
||||||
@ -271,24 +249,5 @@ service ppanel {
|
|||||||
@doc "Query Withdrawal Log"
|
@doc "Query Withdrawal Log"
|
||||||
@handler QueryWithdrawalLog
|
@handler QueryWithdrawalLog
|
||||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||||
|
|
||||||
@doc "Device Online Statistics"
|
|
||||||
@handler DeviceOnlineStatistics
|
|
||||||
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
|
|
||||||
|
|
||||||
@doc "Delete Current User Account"
|
|
||||||
@handler DeleteCurrentUserAccount
|
|
||||||
delete /current_user_account
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@server(
|
|
||||||
prefix: v1/public/user
|
|
||||||
group: public/user/ws
|
|
||||||
middleware: AuthMiddleware
|
|
||||||
)
|
|
||||||
|
|
||||||
service ppanel {
|
|
||||||
@doc "Webosocket Device Connect"
|
|
||||||
@handler DeviceWsConnect
|
|
||||||
get /device_ws_connect
|
|
||||||
}
|
|
||||||
|
|||||||
@ -32,7 +32,6 @@ type (
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||||
IsDel bool `json:"is_del,omitempty"`
|
|
||||||
}
|
}
|
||||||
Follow {
|
Follow {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
@ -151,7 +150,6 @@ type (
|
|||||||
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
||||||
IpRegisterLimit int64 `json:"ip_register_limit"`
|
IpRegisterLimit int64 `json:"ip_register_limit"`
|
||||||
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
||||||
DeviceLimit int64 `json:"device_limit"`
|
|
||||||
}
|
}
|
||||||
VerifyConfig {
|
VerifyConfig {
|
||||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||||
@ -206,7 +204,7 @@ type (
|
|||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
}
|
}
|
||||||
SubscribeDiscount {
|
SubscribeDiscount {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
Subscribe {
|
Subscribe {
|
||||||
@ -448,28 +446,6 @@ type (
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
RedemptionCode {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
TotalCount int64 `json:"total_count"`
|
|
||||||
UsedCount int64 `json:"used_count"`
|
|
||||||
SubscribePlan int64 `json:"subscribe_plan"`
|
|
||||||
UnitTime string `json:"unit_time"`
|
|
||||||
Quantity int64 `json:"quantity"`
|
|
||||||
Status int64 `json:"status"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
|
||||||
RedemptionRecord {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
RedemptionCodeId int64 `json:"redemption_code_id"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
|
||||||
UnitTime string `json:"unit_time"`
|
|
||||||
Quantity int64 `json:"quantity"`
|
|
||||||
RedeemedAt int64 `json:"redeemed_at"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
|
||||||
Announcement {
|
Announcement {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@ -680,7 +656,7 @@ type (
|
|||||||
// public announcement
|
// public announcement
|
||||||
QueryAnnouncementRequest {
|
QueryAnnouncementRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size,default=15"`
|
Size int `form:"size"`
|
||||||
Pinned *bool `form:"pinned"`
|
Pinned *bool `form:"pinned"`
|
||||||
Popup *bool `form:"popup"`
|
Popup *bool `form:"popup"`
|
||||||
}
|
}
|
||||||
@ -697,7 +673,6 @@ type (
|
|||||||
List []SubscribeGroup `json:"list"`
|
List []SubscribeGroup `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetUserSubscribeTrafficLogsRequest {
|
GetUserSubscribeTrafficLogsRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
|
|||||||
BIN
cache/GeoLite2-City.mmdb
vendored
BIN
cache/GeoLite2-City.mmdb
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 61 MiB |
@ -1,73 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/updater"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
checkOnly bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var updateCmd = &cobra.Command{
|
|
||||||
Use: "update",
|
|
||||||
Short: "Check for updates and update PPanel to the latest version",
|
|
||||||
Long: `Check for available updates from GitHub releases and automatically
|
|
||||||
update the PPanel binary to the latest version.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Check for updates only
|
|
||||||
ppanel-server update --check
|
|
||||||
|
|
||||||
# Update to the latest version
|
|
||||||
ppanel-server update`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
u := updater.NewUpdater()
|
|
||||||
|
|
||||||
if checkOnly {
|
|
||||||
checkForUpdates(u)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
performUpdate(u)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
updateCmd.Flags().BoolVarP(&checkOnly, "check", "c", false, "Check for updates without applying them")
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkForUpdates(u *updater.Updater) {
|
|
||||||
fmt.Println("Checking for updates...")
|
|
||||||
|
|
||||||
release, hasUpdate, err := u.CheckForUpdates()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error checking for updates: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasUpdate {
|
|
||||||
fmt.Println("You are already running the latest version!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\nNew version available!\n")
|
|
||||||
fmt.Printf("Current version: %s\n", u.CurrentVersion)
|
|
||||||
fmt.Printf("Latest version: %s\n", release.TagName)
|
|
||||||
fmt.Printf("\nRelease notes:\n%s\n", release.Body)
|
|
||||||
fmt.Printf("\nTo update, run: ppanel-server update\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func performUpdate(u *updater.Updater) {
|
|
||||||
fmt.Println("Starting update process...")
|
|
||||||
|
|
||||||
if err := u.Update(); err != nil {
|
|
||||||
fmt.Printf("Update failed: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("\nUpdate completed successfully!")
|
|
||||||
fmt.Println("Please restart the application to use the new version.")
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
Host: # 服务监听地址
|
|
||||||
Port: 8080 # 服务监听端口, 默认: 8080
|
|
||||||
Debug: true # 是否开启调试模式, 默认: false
|
|
||||||
|
|
||||||
JwtAuth: # JWT认证配置
|
|
||||||
AccessSecret: CHANGE_ME_TO_A_RANDOM_SECRET # 访问令牌密钥, 请修改为随机字符串
|
|
||||||
AccessExpire: 604800 # 访问令牌过期时间,单位秒, 默认: 604800 (7天)
|
|
||||||
|
|
||||||
Logger: # 日志配置
|
|
||||||
FilePath: logs/ppanel.log # 日志文件路径
|
|
||||||
MaxSize: 50 # 日志文件最大大小, 单位MB
|
|
||||||
MaxBackup: 3 # 日志文件最大备份数
|
|
||||||
MaxAge: 30 # 日志文件最大保存时间,单位天
|
|
||||||
Compress: true # 是否压缩日志文件
|
|
||||||
Level: debug # 日志级别: debug, info, warn, error, panic, fatal
|
|
||||||
|
|
||||||
MySQL:
|
|
||||||
Addr: 127.0.0.1:3306 # MySQL地址
|
|
||||||
Username: root # MySQL用户名 (与创建的用户一致)
|
|
||||||
Password: password # MySQL密码 (换成之前生成的随机密码)
|
|
||||||
Dbname: ppanel # MySQL数据库名 (与脚本创建的数据库一致)
|
|
||||||
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
|
||||||
MaxIdleConns: 10
|
|
||||||
MaxOpenConns: 100
|
|
||||||
LogMode: debug
|
|
||||||
LogZap: true
|
|
||||||
SlowThreshold: 1000
|
|
||||||
|
|
||||||
Redis:
|
|
||||||
Host: 127.0.0.1:6379 # Redis地址,格式:host:port
|
|
||||||
Pass: # Redis密码,如果没有设置密码可以留空
|
|
||||||
DB: 0 # Redis数据库编号,默认0
|
|
||||||
PoolSize: 100 # 连接池大小(最大连接数),根据应用并发量调整,建议:小流量50-100,中流量100-300,大流量300-500
|
|
||||||
MinIdleConns: 10 # 最小空闲连接数,保持一定数量的空闲连接以减少建立连接的开销,建议为PoolSize的10%
|
|
||||||
MaxRetries: 3 # 最大重试次数,网络抖动时自动重试,建议2-3次
|
|
||||||
PoolTimeout: 4 # 连接池超时时间(秒),从连接池获取连接的最大等待时间,建议3-5秒
|
|
||||||
IdleTimeout: 300 # 空闲连接超时时间(秒),自动回收长时间空闲的连接,建议300-600秒(5-10分钟)
|
|
||||||
MaxConnAge: 0 # 连接最大生命周期(秒),定期重建连接避免长时间使用的问题,0表示不限制,建议7200秒(2小时)或0
|
|
||||||
DialTimeout: 5 # 连接超时时间(秒),建立新连接的超时时间,建议5秒
|
|
||||||
ReadTimeout: 3 # 读操作超时时间(秒),建议2-3秒
|
|
||||||
WriteTimeout: 3 # 写操作超时时间(秒),建议2-3秒
|
|
||||||
|
|
||||||
Administrator:
|
|
||||||
Email: admin@ppanel.dev # 后台登录邮箱,请修改
|
|
||||||
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码
|
|
||||||
2
go.mod
2
go.mod
@ -27,7 +27,7 @@ require (
|
|||||||
github.com/jinzhu/copier v0.4.0
|
github.com/jinzhu/copier v0.4.0
|
||||||
github.com/klauspost/compress v1.17.7
|
github.com/klauspost/compress v1.17.7
|
||||||
github.com/nyaruka/phonenumbers v1.5.0
|
github.com/nyaruka/phonenumbers v1.5.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/redis/go-redis/v9 v9.7.2
|
github.com/redis/go-redis/v9 v9.7.2
|
||||||
github.com/smartwalle/alipay/v3 v3.2.23
|
github.com/smartwalle/alipay/v3 v3.2.23
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
CREATE INDEX idx_timestamp ON traffic_log (timestamp);
|
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);
|
||||||
|
|
||||||
|
|||||||
@ -1,78 +0,0 @@
|
|||||||
-- Only add the columns to `servers` when they do not already exist
|
|
||||||
|
|
||||||
-- Add longitude
|
|
||||||
SET @col_exists := (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'servers'
|
|
||||||
AND COLUMN_NAME = 'longitude'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @query := IF(
|
|
||||||
@col_exists = 0,
|
|
||||||
'ALTER TABLE `servers` ADD COLUMN `longitude` VARCHAR(255) DEFAULT '''' COMMENT ''longitude'';',
|
|
||||||
'SELECT "Column `longitude` already exists"'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @query;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- Add latitude
|
|
||||||
SET @col_exists := (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'servers'
|
|
||||||
AND COLUMN_NAME = 'latitude'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @query := IF(
|
|
||||||
@col_exists = 0,
|
|
||||||
'ALTER TABLE `servers` ADD COLUMN `latitude` VARCHAR(255) DEFAULT '''' COMMENT ''latitude'';',
|
|
||||||
'SELECT "Column `latitude` already exists"'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @query;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- Add longitude_center
|
|
||||||
SET @col_exists := (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'servers'
|
|
||||||
AND COLUMN_NAME = 'longitude_center'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @query := IF(
|
|
||||||
@col_exists = 0,
|
|
||||||
'ALTER TABLE `servers` ADD COLUMN `longitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''longitude center'';',
|
|
||||||
'SELECT "Column `longitude_center` already exists"'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @query;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- Add latitude_center
|
|
||||||
SET @col_exists := (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'servers'
|
|
||||||
AND COLUMN_NAME = 'latitude_center'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @query := IF(
|
|
||||||
@col_exists = 0,
|
|
||||||
'ALTER TABLE `servers` ADD COLUMN `latitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''latitude center'';',
|
|
||||||
'SELECT "Column `latitude_center` already exists"'
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @query;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
27
initialize/migrate/database/02122_server.down.sql
Normal file
27
initialize/migrate/database/02122_server.down.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `server`
|
||||||
|
(
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name',
|
||||||
|
`tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags',
|
||||||
|
`country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country',
|
||||||
|
`city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City',
|
||||||
|
`latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude',
|
||||||
|
`longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude',
|
||||||
|
`server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address',
|
||||||
|
`relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode',
|
||||||
|
`relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node',
|
||||||
|
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
|
||||||
|
`traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio',
|
||||||
|
`group_id` bigint DEFAULT NULL COMMENT 'Group ID',
|
||||||
|
`protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol',
|
||||||
|
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config',
|
||||||
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled',
|
||||||
|
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
|
||||||
|
`last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time',
|
||||||
|
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
|
||||||
|
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_group_id` (`group_id`)
|
||||||
|
) ENGINE = InnoDB
|
||||||
|
DEFAULT CHARSET = utf8mb4
|
||||||
|
COLLATE = utf8mb4_general_ci;
|
||||||
1
initialize/migrate/database/02122_server.up.sql
Normal file
1
initialize/migrate/database/02122_server.up.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
DROP TABLE IF EXISTS `server`;
|
||||||
@ -1,5 +0,0 @@
|
|||||||
-- Drop redemption_record table
|
|
||||||
DROP TABLE IF EXISTS `redemption_record`;
|
|
||||||
|
|
||||||
-- Drop redemption_code table
|
|
||||||
DROP TABLE IF EXISTS `redemption_code`;
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
-- Create redemption_code table
|
|
||||||
CREATE TABLE IF NOT EXISTS `redemption_code` (
|
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
|
|
||||||
`code` VARCHAR(255) NOT NULL COMMENT 'Redemption Code',
|
|
||||||
`total_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Total Redemption Count',
|
|
||||||
`used_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Used Redemption Count',
|
|
||||||
`subscribe_plan` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Plan',
|
|
||||||
`unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time: day, month, quarter, half_year, year',
|
|
||||||
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity',
|
|
||||||
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
|
|
||||||
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
|
|
||||||
`deleted_at` DATETIME DEFAULT NULL COMMENT 'Deletion Time',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `uk_code` (`code`),
|
|
||||||
KEY `idx_deleted_at` (`deleted_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Code Table';
|
|
||||||
|
|
||||||
-- Create redemption_record table
|
|
||||||
CREATE TABLE IF NOT EXISTS `redemption_record` (
|
|
||||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
|
|
||||||
`redemption_code_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Redemption Code Id',
|
|
||||||
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'User Id',
|
|
||||||
`subscribe_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Id',
|
|
||||||
`unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time',
|
|
||||||
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity',
|
|
||||||
`redeemed_at` DATETIME NOT NULL COMMENT 'Redeemed Time',
|
|
||||||
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_redemption_code_id` (`redemption_code_id`),
|
|
||||||
KEY `idx_user_id` (`user_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Record Table';
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Remove status column from redemption_code table
|
|
||||||
ALTER TABLE `redemption_code` DROP COLUMN `status`;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Add status column to redemption_code table
|
|
||||||
ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=enabled, 0=disabled' AFTER `quantity`;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Remove device limit configuration from system table
|
|
||||||
DELETE FROM `system` WHERE `category` = 'register' AND `key` = 'DeviceLimit';
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
-- Add device limit configuration to system table
|
|
||||||
INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
|
|
||||||
VALUES (43, 'register', 'DeviceLimit', '5', 'int', 'Device binding limit', NOW(), NOW());
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Remove short_code column from user_device table
|
|
||||||
ALTER TABLE `user_device` DROP COLUMN `short_code`;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Add short_code column to user_device table
|
|
||||||
ALTER TABLE `user_device` ADD COLUMN `short_code` VARCHAR(255) DEFAULT '' COMMENT 'Short Code' AFTER `identifier`;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Remove index on refer_code column
|
|
||||||
ALTER TABLE `user` DROP INDEX `idx_refer_code`;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
-- Add index on refer_code column for faster lookup
|
|
||||||
ALTER TABLE `user` ADD INDEX `idx_refer_code` (`refer_code`);
|
|
||||||
@ -39,9 +39,6 @@ const VerifyCodeConfigKey = "system:verify_code_config"
|
|||||||
// SessionIdKey cache session key
|
// SessionIdKey cache session key
|
||||||
const SessionIdKey = "auth:session_id"
|
const SessionIdKey = "auth:session_id"
|
||||||
|
|
||||||
// DeviceCacheKeyKey cache session key
|
|
||||||
const DeviceCacheKeyKey = "auth:device_identifier"
|
|
||||||
|
|
||||||
// GlobalConfigKey Global Config Key
|
// GlobalConfigKey Global Config Key
|
||||||
const GlobalConfigKey = "system:global_config"
|
const GlobalConfigKey = "system:global_config"
|
||||||
|
|
||||||
@ -62,5 +59,3 @@ const SendIntervalKeyPrefix = "send:interval:"
|
|||||||
|
|
||||||
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
|
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
|
||||||
const SendCountLimitKeyPrefix = "send:limit:"
|
const SendCountLimitKeyPrefix = "send:limit:"
|
||||||
|
|
||||||
const RegisterIpKeyPrefix = "register:ip:"
|
|
||||||
|
|||||||
@ -37,18 +37,9 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RedisConfig struct {
|
type RedisConfig struct {
|
||||||
Host string `yaml:"Host" default:"localhost:6379"`
|
Host string `yaml:"Host" default:"localhost:6379"`
|
||||||
Pass string `yaml:"Pass" default:""`
|
Pass string `yaml:"Pass" default:""`
|
||||||
DB int `yaml:"DB" default:"0"`
|
DB int `yaml:"DB" default:"0"`
|
||||||
PoolSize int `yaml:"PoolSize" default:"100"` // 连接池大小(最大连接数)
|
|
||||||
MinIdleConns int `yaml:"MinIdleConns" default:"10"` // 最小空闲连接数
|
|
||||||
MaxRetries int `yaml:"MaxRetries" default:"3"` // 最大重试次数
|
|
||||||
PoolTimeout int `yaml:"PoolTimeout" default:"4"` // 连接池超时时间(秒)
|
|
||||||
IdleTimeout int `yaml:"IdleTimeout" default:"300"` // 空闲连接超时时间(秒)
|
|
||||||
MaxConnAge int `yaml:"MaxConnAge" default:"0"` // 连接最大生命周期(秒),0表示不限制
|
|
||||||
DialTimeout int `yaml:"DialTimeout" default:"5"` // 连接超时时间(秒)
|
|
||||||
ReadTimeout int `yaml:"ReadTimeout" default:"3"` // 读超时时间(秒)
|
|
||||||
WriteTimeout int `yaml:"WriteTimeout" default:"3"` // 写超时时间(秒)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type JwtAuth struct {
|
type JwtAuth struct {
|
||||||
@ -82,7 +73,6 @@ type RegisterConfig struct {
|
|||||||
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
||||||
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
||||||
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
||||||
DeviceLimit int64 `yaml:"DeviceLimit" default:"5"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Batch delete redemption code
|
|
||||||
func BatchDeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.BatchDeleteRedemptionCodeRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewBatchDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.BatchDeleteRedemptionCode(&req)
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create redemption code
|
|
||||||
func CreateRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.CreateRedemptionCodeRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewCreateRedemptionCodeLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.CreateRedemptionCode(&req)
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Delete redemption code
|
|
||||||
func DeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.DeleteRedemptionCodeRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.DeleteRedemptionCode(&req)
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get redemption code list
|
|
||||||
func GetRedemptionCodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.GetRedemptionCodeListRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewGetRedemptionCodeListLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.GetRedemptionCodeList(&req)
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get redemption record list
|
|
||||||
func GetRedemptionRecordListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.GetRedemptionRecordListRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewGetRedemptionRecordListLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.GetRedemptionRecordList(&req)
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Toggle redemption code status
|
|
||||||
func ToggleRedemptionCodeStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.ToggleRedemptionCodeStatusRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewToggleRedemptionCodeStatusLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.ToggleRedemptionCodeStatus(&req)
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update redemption code
|
|
||||||
func UpdateRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.UpdateRedemptionCodeRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewUpdateRedemptionCodeLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.UpdateRedemptionCode(&req)
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/public/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Redeem code
|
|
||||||
func RedeemCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
var req types.RedeemCodeRequest
|
|
||||||
_ = c.ShouldBind(&req)
|
|
||||||
validateErr := svcCtx.Validate(&req)
|
|
||||||
if validateErr != nil {
|
|
||||||
result.ParamErrorResult(c, validateErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := redemption.NewRedeemCodeLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.RedeemCode(&req)
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Delete Current User Account
|
|
||||||
func DeleteCurrentUserAccountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
|
|
||||||
l := user.NewDeleteCurrentUserAccountLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.DeleteCurrentUserAccount()
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Device Online Statistics
|
|
||||||
func DeviceOnlineStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
|
|
||||||
l := user.NewDeviceOnlineStatisticsLogic(c.Request.Context(), svcCtx)
|
|
||||||
resp, err := l.DeviceOnlineStatistics()
|
|
||||||
result.HttpResult(c, resp, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package ws
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
wslogic "github.com/perfect-panel/server/internal/logic/public/user/ws"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upGrader = websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
return true // 允许所有来源,生产环境中应该根据需求限制
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webosocket Device Connect
|
|
||||||
func DeviceWsConnectHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
|
|
||||||
l := wslogic.NewDeviceWsConnectLogic(c.Request.Context(), svcCtx)
|
|
||||||
err := l.DeviceWsConnect(c)
|
|
||||||
result.HttpResult(c, nil, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -16,7 +16,6 @@ import (
|
|||||||
adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing"
|
adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing"
|
||||||
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
|
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
|
||||||
adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment"
|
adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment"
|
||||||
adminRedemption "github.com/perfect-panel/server/internal/handler/admin/redemption"
|
|
||||||
adminServer "github.com/perfect-panel/server/internal/handler/admin/server"
|
adminServer "github.com/perfect-panel/server/internal/handler/admin/server"
|
||||||
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
|
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
|
||||||
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system"
|
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system"
|
||||||
@ -31,11 +30,9 @@ import (
|
|||||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||||
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
||||||
publicRedemption "github.com/perfect-panel/server/internal/handler/public/redemption"
|
|
||||||
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
|
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
|
||||||
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
|
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
|
||||||
publicUser "github.com/perfect-panel/server/internal/handler/public/user"
|
publicUser "github.com/perfect-panel/server/internal/handler/public/user"
|
||||||
publicUserWs "github.com/perfect-panel/server/internal/handler/public/user/ws"
|
|
||||||
server "github.com/perfect-panel/server/internal/handler/server"
|
server "github.com/perfect-panel/server/internal/handler/server"
|
||||||
"github.com/perfect-panel/server/internal/middleware"
|
"github.com/perfect-panel/server/internal/middleware"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@ -301,32 +298,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx))
|
adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
adminRedemptionGroupRouter := router.Group("/v1/admin/redemption")
|
|
||||||
adminRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
|
||||||
|
|
||||||
{
|
|
||||||
// Create redemption code
|
|
||||||
adminRedemptionGroupRouter.POST("/code", adminRedemption.CreateRedemptionCodeHandler(serverCtx))
|
|
||||||
|
|
||||||
// Update redemption code
|
|
||||||
adminRedemptionGroupRouter.PUT("/code", adminRedemption.UpdateRedemptionCodeHandler(serverCtx))
|
|
||||||
|
|
||||||
// Delete redemption code
|
|
||||||
adminRedemptionGroupRouter.DELETE("/code", adminRedemption.DeleteRedemptionCodeHandler(serverCtx))
|
|
||||||
|
|
||||||
// Batch delete redemption code
|
|
||||||
adminRedemptionGroupRouter.DELETE("/code/batch", adminRedemption.BatchDeleteRedemptionCodeHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get redemption code list
|
|
||||||
adminRedemptionGroupRouter.GET("/code/list", adminRedemption.GetRedemptionCodeListHandler(serverCtx))
|
|
||||||
|
|
||||||
// Toggle redemption code status
|
|
||||||
adminRedemptionGroupRouter.PUT("/code/status", adminRedemption.ToggleRedemptionCodeStatusHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get redemption record list
|
|
||||||
adminRedemptionGroupRouter.GET("/record/list", adminRedemption.GetRedemptionRecordListHandler(serverCtx))
|
|
||||||
}
|
|
||||||
|
|
||||||
adminServerGroupRouter := router.Group("/v1/admin/server")
|
adminServerGroupRouter := router.Group("/v1/admin/server")
|
||||||
adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -777,14 +748,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx))
|
publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
publicRedemptionGroupRouter := router.Group("/v1/public/redemption")
|
|
||||||
publicRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
|
||||||
|
|
||||||
{
|
|
||||||
// Redeem code
|
|
||||||
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
|
|
||||||
}
|
|
||||||
|
|
||||||
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
||||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -850,12 +813,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Commission Withdraw
|
// Commission Withdraw
|
||||||
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
||||||
|
|
||||||
// Delete Current User Account
|
|
||||||
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
|
||||||
|
|
||||||
// Device Online Statistics
|
|
||||||
publicUserGroupRouter.GET("/device_online_statistics", publicUser.DeviceOnlineStatisticsHandler(serverCtx))
|
|
||||||
|
|
||||||
// Get Device List
|
// Get Device List
|
||||||
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
|
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
|
||||||
|
|
||||||
@ -911,14 +868,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
|
||||||
publicUserWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
|
||||||
|
|
||||||
{
|
|
||||||
// Webosocket Device Connect
|
|
||||||
publicUserWsGroupRouter.GET("/device_ws_connect", publicUserWs.DeviceWsConnectHandler(serverCtx))
|
|
||||||
}
|
|
||||||
|
|
||||||
serverGroupRouter := router.Group("/v1/server")
|
serverGroupRouter := router.Group("/v1/server")
|
||||||
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))
|
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -939,10 +888,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverV2GroupRouter := router.Group("/v2/server")
|
serverGroupRouter := router.Group("/v2/server")
|
||||||
|
|
||||||
{
|
{
|
||||||
// Get Server Protocol Config
|
// Get Server Protocol Config
|
||||||
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type BatchDeleteRedemptionCodeLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch delete redemption code
|
|
||||||
func NewBatchDeleteRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteRedemptionCodeLogic {
|
|
||||||
return &BatchDeleteRedemptionCodeLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *BatchDeleteRedemptionCodeLogic) BatchDeleteRedemptionCode(req *types.BatchDeleteRedemptionCodeRequest) error {
|
|
||||||
err := l.svcCtx.RedemptionCodeModel.BatchDelete(l.ctx, req.Ids)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[BatchDeleteRedemptionCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "batch delete redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,120 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/redemption"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CreateRedemptionCodeLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create redemption code
|
|
||||||
func NewCreateRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRedemptionCodeLogic {
|
|
||||||
return &CreateRedemptionCodeLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateUniqueCode generates a unique redemption code
|
|
||||||
func (l *CreateRedemptionCodeLogic) generateUniqueCode() (string, error) {
|
|
||||||
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Removed confusing characters like I, O, 0, 1
|
|
||||||
const codeLength = 16
|
|
||||||
|
|
||||||
maxRetries := 10
|
|
||||||
for i := 0; i < maxRetries; i++ {
|
|
||||||
code := make([]byte, codeLength)
|
|
||||||
for j := 0; j < codeLength; j++ {
|
|
||||||
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
code[j] = charset[num.Int64()]
|
|
||||||
}
|
|
||||||
|
|
||||||
codeStr := string(code)
|
|
||||||
|
|
||||||
// Check if code already exists
|
|
||||||
_, err := l.svcCtx.RedemptionCodeModel.FindOneByCode(l.ctx, codeStr)
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return codeStr, nil
|
|
||||||
} else if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Code exists, try again
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("failed to generate unique code after maximum retries")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *CreateRedemptionCodeLogic) CreateRedemptionCode(req *types.CreateRedemptionCodeRequest) error {
|
|
||||||
// Check if subscribe plan is valid
|
|
||||||
if req.SubscribePlan == 0 {
|
|
||||||
l.Errorw("[CreateRedemptionCode] Subscribe plan cannot be empty")
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "subscribe plan cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify subscribe plan exists
|
|
||||||
_, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribePlan)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
l.Errorw("[CreateRedemptionCode] Subscribe plan not found", logger.Field("subscribe_plan", req.SubscribePlan))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "subscribe plan not found")
|
|
||||||
}
|
|
||||||
l.Errorw("[CreateRedemptionCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe plan error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate batch count
|
|
||||||
if req.BatchCount < 1 {
|
|
||||||
l.Errorw("[CreateRedemptionCode] Batch count must be at least 1")
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "batch count must be at least 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate redemption codes in batch
|
|
||||||
var createdCodes []string
|
|
||||||
for i := int64(0); i < req.BatchCount; i++ {
|
|
||||||
code, err := l.generateUniqueCode()
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[CreateRedemptionCode] Failed to generate unique code", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "generate unique code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
redemptionCode := &redemption.RedemptionCode{
|
|
||||||
Code: code,
|
|
||||||
TotalCount: req.TotalCount,
|
|
||||||
UsedCount: 0,
|
|
||||||
SubscribePlan: req.SubscribePlan,
|
|
||||||
UnitTime: req.UnitTime,
|
|
||||||
Quantity: req.Quantity,
|
|
||||||
Status: 1, // Default to enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.svcCtx.RedemptionCodeModel.Insert(l.ctx, redemptionCode)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[CreateRedemptionCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
createdCodes = append(createdCodes, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("[CreateRedemptionCode] Successfully created redemption codes",
|
|
||||||
logger.Field("count", len(createdCodes)),
|
|
||||||
logger.Field("codes", createdCodes))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeleteRedemptionCodeLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete redemption code
|
|
||||||
func NewDeleteRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRedemptionCodeLogic {
|
|
||||||
return &DeleteRedemptionCodeLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *DeleteRedemptionCodeLogic) DeleteRedemptionCode(req *types.DeleteRedemptionCodeRequest) error {
|
|
||||||
err := l.svcCtx.RedemptionCodeModel.Delete(l.ctx, req.Id)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[DeleteRedemptionCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetRedemptionCodeListLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get redemption code list
|
|
||||||
func NewGetRedemptionCodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRedemptionCodeListLogic {
|
|
||||||
return &GetRedemptionCodeListLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *GetRedemptionCodeListLogic) GetRedemptionCodeList(req *types.GetRedemptionCodeListRequest) (resp *types.GetRedemptionCodeListResponse, err error) {
|
|
||||||
total, list, err := l.svcCtx.RedemptionCodeModel.QueryRedemptionCodeListByPage(
|
|
||||||
l.ctx,
|
|
||||||
int(req.Page),
|
|
||||||
int(req.Size),
|
|
||||||
req.SubscribePlan,
|
|
||||||
req.UnitTime,
|
|
||||||
req.Code,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[GetRedemptionCodeList] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get redemption code list error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var redemptionCodes []types.RedemptionCode
|
|
||||||
for _, item := range list {
|
|
||||||
redemptionCodes = append(redemptionCodes, types.RedemptionCode{
|
|
||||||
Id: item.Id,
|
|
||||||
Code: item.Code,
|
|
||||||
TotalCount: item.TotalCount,
|
|
||||||
UsedCount: item.UsedCount,
|
|
||||||
SubscribePlan: item.SubscribePlan,
|
|
||||||
UnitTime: item.UnitTime,
|
|
||||||
Quantity: item.Quantity,
|
|
||||||
Status: item.Status,
|
|
||||||
CreatedAt: item.CreatedAt.Unix(),
|
|
||||||
UpdatedAt: item.UpdatedAt.Unix(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.GetRedemptionCodeListResponse{
|
|
||||||
Total: total,
|
|
||||||
List: redemptionCodes,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GetRedemptionRecordListLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get redemption record list
|
|
||||||
func NewGetRedemptionRecordListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRedemptionRecordListLogic {
|
|
||||||
return &GetRedemptionRecordListLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *GetRedemptionRecordListLogic) GetRedemptionRecordList(req *types.GetRedemptionRecordListRequest) (resp *types.GetRedemptionRecordListResponse, err error) {
|
|
||||||
total, list, err := l.svcCtx.RedemptionRecordModel.QueryRedemptionRecordListByPage(
|
|
||||||
l.ctx,
|
|
||||||
int(req.Page),
|
|
||||||
int(req.Size),
|
|
||||||
req.UserId,
|
|
||||||
req.CodeId,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[GetRedemptionRecordList] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get redemption record list error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var redemptionRecords []types.RedemptionRecord
|
|
||||||
for _, item := range list {
|
|
||||||
redemptionRecords = append(redemptionRecords, types.RedemptionRecord{
|
|
||||||
Id: item.Id,
|
|
||||||
RedemptionCodeId: item.RedemptionCodeId,
|
|
||||||
UserId: item.UserId,
|
|
||||||
SubscribeId: item.SubscribeId,
|
|
||||||
UnitTime: item.UnitTime,
|
|
||||||
Quantity: item.Quantity,
|
|
||||||
RedeemedAt: item.RedeemedAt.Unix(),
|
|
||||||
CreatedAt: item.CreatedAt.Unix(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &types.GetRedemptionRecordListResponse{
|
|
||||||
Total: total,
|
|
||||||
List: redemptionRecords,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ToggleRedemptionCodeStatusLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle redemption code status
|
|
||||||
func NewToggleRedemptionCodeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToggleRedemptionCodeStatusLogic {
|
|
||||||
return &ToggleRedemptionCodeStatusLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ToggleRedemptionCodeStatusLogic) ToggleRedemptionCodeStatus(req *types.ToggleRedemptionCodeStatusRequest) error {
|
|
||||||
// Find redemption code
|
|
||||||
codeInfo, err := l.svcCtx.RedemptionCodeModel.FindOne(l.ctx, req.Id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
l.Errorw("[ToggleRedemptionCodeStatus] Redemption code not found", logger.Field("id", req.Id))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code not found")
|
|
||||||
}
|
|
||||||
l.Errorw("[ToggleRedemptionCodeStatus] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status
|
|
||||||
codeInfo.Status = req.Status
|
|
||||||
|
|
||||||
err = l.svcCtx.RedemptionCodeModel.Update(l.ctx, codeInfo)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[ToggleRedemptionCodeStatus] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update redemption code status error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("[ToggleRedemptionCodeStatus] Successfully toggled redemption code status",
|
|
||||||
logger.Field("id", req.Id),
|
|
||||||
logger.Field("status", req.Status))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UpdateRedemptionCodeLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update redemption code
|
|
||||||
func NewUpdateRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRedemptionCodeLogic {
|
|
||||||
return &UpdateRedemptionCodeLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *UpdateRedemptionCodeLogic) UpdateRedemptionCode(req *types.UpdateRedemptionCodeRequest) error {
|
|
||||||
redemptionCode, err := l.svcCtx.RedemptionCodeModel.FindOne(l.ctx, req.Id)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[UpdateRedemptionCode] Find Redemption Code Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Code is not allowed to be modified
|
|
||||||
if req.TotalCount != 0 {
|
|
||||||
// Total count cannot be less than used count
|
|
||||||
if req.TotalCount < redemptionCode.UsedCount {
|
|
||||||
l.Errorw("[UpdateRedemptionCode] Total count cannot be less than used count",
|
|
||||||
logger.Field("total_count", req.TotalCount),
|
|
||||||
logger.Field("used_count", redemptionCode.UsedCount))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams),
|
|
||||||
"total count cannot be less than used count: total_count=%d, used_count=%d",
|
|
||||||
req.TotalCount, redemptionCode.UsedCount)
|
|
||||||
}
|
|
||||||
redemptionCode.TotalCount = req.TotalCount
|
|
||||||
}
|
|
||||||
if req.SubscribePlan != 0 {
|
|
||||||
redemptionCode.SubscribePlan = req.SubscribePlan
|
|
||||||
}
|
|
||||||
if req.UnitTime != "" {
|
|
||||||
redemptionCode.UnitTime = req.UnitTime
|
|
||||||
}
|
|
||||||
if req.Quantity != 0 {
|
|
||||||
redemptionCode.Quantity = req.Quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.svcCtx.RedemptionCodeModel.Update(l.ctx, redemptionCode)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[UpdateRedemptionCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -99,10 +99,6 @@ func (l *CreateServerLogic) CreateServer(req *types.CreateServerRequest) error {
|
|||||||
} else {
|
} else {
|
||||||
data.City = result.City
|
data.City = result.City
|
||||||
data.Country = result.Country
|
data.Country = result.Country
|
||||||
data.Latitude = result.Latitude
|
|
||||||
data.Longitude = result.Longitude
|
|
||||||
data.LatitudeCenter = result.LatitudeCenter
|
|
||||||
data.LongitudeCenter = result.LongitudeCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data)
|
err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data)
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
|
|||||||
data.Country = req.Country
|
data.Country = req.Country
|
||||||
data.City = req.City
|
data.City = req.City
|
||||||
// only update address when it's different
|
// only update address when it's different
|
||||||
if req.Address != data.Address || (data.Country == "" || req.Country == "") {
|
if req.Address != data.Address {
|
||||||
// query server ip location
|
// query server ip location
|
||||||
result, err := ip.GetRegionByIp(req.Address)
|
result, err := ip.GetRegionByIp(req.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,10 +47,6 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
|
|||||||
} else {
|
} else {
|
||||||
data.City = result.City
|
data.City = result.City
|
||||||
data.Country = result.Country
|
data.Country = result.Country
|
||||||
data.Latitude = result.Latitude
|
|
||||||
data.Longitude = result.Longitude
|
|
||||||
data.LatitudeCenter = result.LatitudeCenter
|
|
||||||
data.LongitudeCenter = result.LongitudeCenter
|
|
||||||
}
|
}
|
||||||
// update address
|
// update address
|
||||||
data.Address = req.Address
|
data.Address = req.Address
|
||||||
|
|||||||
@ -2,12 +2,14 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"os"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetModuleConfigLogic struct {
|
type GetModuleConfigLogic struct {
|
||||||
@ -26,14 +28,14 @@ func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *G
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
|
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
|
||||||
//value, exists := os.LookupEnv("SECRET_KEY")
|
value, exists := os.LookupEnv("SECRET_KEY")
|
||||||
//if !exists {
|
if !exists {
|
||||||
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
||||||
//}
|
}
|
||||||
|
|
||||||
return &types.ModuleConfig{
|
return &types.ModuleConfig{
|
||||||
//Secret: value,
|
Secret: value,
|
||||||
ServiceName: constant.ServiceName,
|
ServiceName: constant.ServiceName,
|
||||||
ServiceVersion: strings.ReplaceAll(constant.Version, "v", ""),
|
ServiceVersion: constant.Version,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,9 +48,5 @@ func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubs
|
|||||||
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId))
|
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
||||||
}
|
}
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear server cache: %v", err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,6 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
|||||||
Unscoped: req.Unscoped,
|
Unscoped: req.Unscoped,
|
||||||
SubscribeId: req.SubscribeId,
|
SubscribeId: req.SubscribeId,
|
||||||
UserSubscribeId: req.UserSubscribeId,
|
UserSubscribeId: req.UserSubscribeId,
|
||||||
ShortCode: req.ShortCode,
|
|
||||||
Order: "DESC",
|
Order: "DESC",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -69,10 +69,5 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs
|
|||||||
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
|
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear server cache: %v", err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@ -89,36 +88,6 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
|||||||
logger.Field("user_id", userId),
|
logger.Field("user_id", userId),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check device limit
|
|
||||||
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
|
|
||||||
if deviceLimit > 0 {
|
|
||||||
// Count current user's devices
|
|
||||||
var deviceCount int64
|
|
||||||
if err := l.svcCtx.DB.Model(&user.Device{}).Where("user_id = ?", userId).Count(&deviceCount).Error; err != nil {
|
|
||||||
l.Errorw("failed to count user devices",
|
|
||||||
logger.Field("user_id", userId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count devices failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if limit reached
|
|
||||||
if deviceCount >= deviceLimit {
|
|
||||||
l.Errorw("device limit reached",
|
|
||||||
logger.Field("user_id", userId),
|
|
||||||
logger.Field("device_count", deviceCount),
|
|
||||||
logger.Field("device_limit", deviceLimit),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device limit reached: maximum %d devices allowed", deviceLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("device limit check passed",
|
|
||||||
logger.Field("user_id", userId),
|
|
||||||
logger.Field("device_count", deviceCount),
|
|
||||||
logger.Field("device_limit", deviceLimit),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
// Create device auth method
|
// Create device auth method
|
||||||
authMethod := &user.AuthMethods{
|
authMethod := &user.AuthMethods{
|
||||||
@ -138,9 +107,8 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
|||||||
|
|
||||||
// Create device record
|
// Create device record
|
||||||
deviceInfo := &user.Device{
|
deviceInfo := &user.Device{
|
||||||
Ip: ip,
|
Ip: ip,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
|
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
Identifier: identifier,
|
Identifier: identifier,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@ -178,87 +146,10 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
|||||||
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
|
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
|
||||||
oldUserId := deviceInfo.UserId
|
oldUserId := deviceInfo.UserId
|
||||||
|
|
||||||
// Check device limit for new user
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
|
// Check if old user has other auth methods besides device
|
||||||
if deviceLimit > 0 {
|
var authMethods []user.AuthMethods
|
||||||
// Count new user's current devices (excluding the one being rebound)
|
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
|
||||||
var deviceCount int64
|
|
||||||
if err := l.svcCtx.DB.Model(&user.Device{}).Where("user_id = ? AND id != ?", newUserId, deviceInfo.Id).Count(&deviceCount).Error; err != nil {
|
|
||||||
l.Errorw("failed to count new user devices",
|
|
||||||
logger.Field("user_id", newUserId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count devices failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if limit reached
|
|
||||||
if deviceCount >= deviceLimit {
|
|
||||||
l.Errorw("device limit reached for new user",
|
|
||||||
logger.Field("user_id", newUserId),
|
|
||||||
logger.Field("device_count", deviceCount),
|
|
||||||
logger.Field("device_limit", deviceLimit),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device limit reached: maximum %d devices allowed", deviceLimit)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("device limit check passed for rebinding",
|
|
||||||
logger.Field("user_id", newUserId),
|
|
||||||
logger.Field("device_count", deviceCount),
|
|
||||||
logger.Field("device_limit", deviceLimit),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var users []*user.User
|
|
||||||
err := l.svcCtx.DB.Where("id in (?)", []int64{oldUserId, newUserId}).Find(&users).Error
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("failed to query users for rebinding",
|
|
||||||
logger.Field("old_user_id", oldUserId),
|
|
||||||
logger.Field("new_user_id", newUserId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query users failed: %v", err)
|
|
||||||
}
|
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
|
||||||
//检查旧设备是否存在认证方式
|
|
||||||
var authMethod user.AuthMethods
|
|
||||||
err := tx.Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).Find(&authMethod).Error
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
l.Errorw("failed to query device auth method",
|
|
||||||
logger.Field("identifier", deviceInfo.Identifier),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device auth method failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//未找到设备认证方式信息,创建新的设备认证方式
|
|
||||||
if err != nil {
|
|
||||||
authMethod = user.AuthMethods{
|
|
||||||
UserId: newUserId,
|
|
||||||
AuthType: "device",
|
|
||||||
AuthIdentifier: deviceInfo.Identifier,
|
|
||||||
Verified: true,
|
|
||||||
}
|
|
||||||
logger.Infof("create auth method: %v", authMethod)
|
|
||||||
if err := tx.Create(&authMethod).Error; err != nil {
|
|
||||||
l.Errorw("failed to create device auth method", logger.Field("new_user_id", newUserId),
|
|
||||||
logger.Field("error", err.Error()))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//更新设备认证方式的用户ID为新用户ID
|
|
||||||
authMethod.UserId = newUserId
|
|
||||||
if err := tx.Save(&authMethod).Error; err != nil {
|
|
||||||
l.Errorw("failed to update device auth method",
|
|
||||||
logger.Field("identifier", deviceInfo.Identifier),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//检查旧用户是否还有其他认证方式
|
|
||||||
var count int64
|
|
||||||
if err := tx.Model(&user.AuthMethods{}).Where("user_id = ?", oldUserId).Count(&count).Error; err != nil {
|
|
||||||
l.Errorw("failed to query auth methods for old user",
|
l.Errorw("failed to query auth methods for old user",
|
||||||
logger.Field("old_user_id", oldUserId),
|
logger.Field("old_user_id", oldUserId),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
@ -266,113 +157,60 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
|
|||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
//如果没有其他认证方式,禁用旧用户账号
|
// Count non-device auth methods
|
||||||
if count < 1 {
|
nonDeviceAuthCount := 0
|
||||||
//检查设备下是否有套餐,有套餐。就检查即将绑定过去的所有账户是否有套餐,如果有,那么检查两个套餐是否一致。如果一致就将即将删除的用户套餐,时间叠加到我绑定过去的用户套餐上面(如果套餐已过期就忽略)。新绑定设备的账户上套餐不一致或者不存在直接将套餐换绑即可
|
for _, auth := range authMethods {
|
||||||
var oldUserSubscribes []user.Subscribe
|
if auth.AuthType != "device" {
|
||||||
err = tx.Where("user_id = ? AND status IN ?", oldUserId, []int64{0, 1}).Find(&oldUserSubscribes).Error
|
nonDeviceAuthCount++
|
||||||
if err != nil {
|
|
||||||
l.Errorw("failed to query old user subscribes",
|
|
||||||
logger.Field("old_user_id", oldUserId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query old user subscribes failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(oldUserSubscribes) > 0 {
|
// Only disable old user if they have no other auth methods
|
||||||
l.Infow("processing old user subscribes",
|
if nonDeviceAuthCount == 0 {
|
||||||
logger.Field("old_user_id", oldUserId),
|
falseVal := false
|
||||||
logger.Field("new_user_id", newUserId),
|
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
|
||||||
logger.Field("subscribe_count", len(oldUserSubscribes)),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, oldSub := range oldUserSubscribes {
|
|
||||||
// 检查新用户是否有相同套餐ID的订阅
|
|
||||||
var newUserSub user.Subscribe
|
|
||||||
err = tx.Where("user_id = ? AND subscribe_id = ? AND status IN ?", newUserId, oldSub.SubscribeId, []int64{0, 1}).First(&newUserSub).Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// 新用户没有该套餐,直接换绑
|
|
||||||
oldSub.UserId = newUserId
|
|
||||||
if err := tx.Save(&oldSub).Error; err != nil {
|
|
||||||
l.Errorw("failed to rebind subscribe to new user",
|
|
||||||
logger.Field("subscribe_id", oldSub.Id),
|
|
||||||
logger.Field("old_user_id", oldUserId),
|
|
||||||
logger.Field("new_user_id", newUserId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "rebind subscribe failed: %v", err)
|
|
||||||
}
|
|
||||||
l.Infow("rebind subscribe to new user",
|
|
||||||
logger.Field("subscribe_id", oldSub.Id),
|
|
||||||
logger.Field("new_user_id", newUserId),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// 新用户已有该套餐,检查旧套餐是否过期
|
|
||||||
now := time.Now()
|
|
||||||
if oldSub.ExpireTime.After(now) {
|
|
||||||
// 旧套餐未过期,叠加剩余时间
|
|
||||||
remainingDuration := oldSub.ExpireTime.Sub(now)
|
|
||||||
if newUserSub.ExpireTime.After(now) {
|
|
||||||
// 新套餐未过期,叠加时间
|
|
||||||
newUserSub.ExpireTime = newUserSub.ExpireTime.Add(remainingDuration)
|
|
||||||
} else {
|
|
||||||
newUserSub.ExpireTime = time.Now().Add(remainingDuration)
|
|
||||||
}
|
|
||||||
if err := tx.Save(&newUserSub).Error; err != nil {
|
|
||||||
l.Errorw("failed to update subscribe expire time",
|
|
||||||
logger.Field("subscribe_id", newUserSub.Id),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe expire time failed: %v", err)
|
|
||||||
}
|
|
||||||
l.Infow("merged subscribe time",
|
|
||||||
logger.Field("subscribe_id", newUserSub.Id),
|
|
||||||
logger.Field("new_expire_time", newUserSub.ExpireTime),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
l.Infow("old subscribe expired, skip merge",
|
|
||||||
logger.Field("subscribe_id", oldSub.Id),
|
|
||||||
logger.Field("expire_time", oldSub.ExpireTime),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 删除旧用户的套餐
|
|
||||||
if err := tx.Delete(&oldSub).Error; err != nil {
|
|
||||||
l.Errorw("failed to delete old subscribe",
|
|
||||||
logger.Field("subscribe_id", oldSub.Id),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete old subscribe failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Model(&user.User{}).Where("id = ?", oldUserId).Delete(&user.User{}).Error; err != nil {
|
|
||||||
l.Errorw("failed to disable old user",
|
l.Errorw("failed to disable old user",
|
||||||
logger.Field("old_user_id", oldUserId),
|
logger.Field("old_user_id", oldUserId),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
l.Infow("disabled old user (no other auth methods)",
|
||||||
|
logger.Field("old_user_id", oldUserId),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
l.Infow("old user has other auth methods, not disabling",
|
||||||
|
logger.Field("old_user_id", oldUserId),
|
||||||
|
logger.Field("non_device_auth_count", nonDeviceAuthCount),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infow("disabled old user (no other auth methods)",
|
// Update device auth method to new user
|
||||||
logger.Field("old_user_id", oldUserId),
|
if err := db.Model(&user.AuthMethods{}).
|
||||||
)
|
Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).
|
||||||
|
Update("user_id", newUserId).Error; err != nil {
|
||||||
|
l.Errorw("failed to update device auth method",
|
||||||
|
logger.Field("identifier", deviceInfo.Identifier),
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
)
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// 更新设备绑定的用户id
|
// Update device record
|
||||||
deviceInfo.UserId = newUserId
|
deviceInfo.UserId = newUserId
|
||||||
deviceInfo.Ip = ip
|
deviceInfo.Ip = ip
|
||||||
deviceInfo.UserAgent = userAgent
|
deviceInfo.UserAgent = userAgent
|
||||||
deviceInfo.Enabled = true
|
deviceInfo.Enabled = true
|
||||||
if err := tx.Save(deviceInfo).Error; err != nil {
|
|
||||||
|
if err := db.Save(deviceInfo).Error; err != nil {
|
||||||
l.Errorw("failed to update device",
|
l.Errorw("failed to update device",
|
||||||
logger.Field("identifier", deviceInfo.Identifier),
|
logger.Field("identifier", deviceInfo.Identifier),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -386,15 +224,6 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = l.svcCtx.UserModel.ClearUserCache(l.ctx, users...)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("failed to clear user cache after rebinding",
|
|
||||||
logger.Field("old_user_id", oldUserId),
|
|
||||||
logger.Field("new_user_id", newUserId),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("device rebound successfully",
|
l.Infow("device rebound successfully",
|
||||||
logger.Field("identifier", deviceInfo.Identifier),
|
logger.Field("identifier", deviceInfo.Identifier),
|
||||||
logger.Field("old_user_id", oldUserId),
|
logger.Field("old_user_id", oldUserId),
|
||||||
|
|||||||
@ -71,9 +71,6 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
|||||||
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "device", req.Identifier) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
|
|
||||||
}
|
|
||||||
// Device not found, create new user and device
|
// Device not found, create new user and device
|
||||||
userInfo, err = l.registerUserAndDevice(req)
|
userInfo, err = l.registerUserAndDevice(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -128,17 +125,6 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store device id in redis
|
|
||||||
|
|
||||||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
|
||||||
if err = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
|
||||||
l.Errorw("set device id error",
|
|
||||||
logger.Field("user_id", userInfo.Id),
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
)
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set device id error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
loginStatus = true
|
loginStatus = true
|
||||||
return &types.LoginResponse{
|
return &types.LoginResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
@ -152,11 +138,9 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
)
|
)
|
||||||
|
|
||||||
var userInfo *user.User
|
var userInfo *user.User
|
||||||
var trialSubscribe *user.Subscribe
|
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
// Create new user
|
// Create new user
|
||||||
userInfo = &user.User{
|
userInfo = &user.User{
|
||||||
Salt: "default",
|
|
||||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||||
}
|
}
|
||||||
if err := db.Create(userInfo).Error; err != nil {
|
if err := db.Create(userInfo).Error; err != nil {
|
||||||
@ -198,7 +182,6 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
UserId: userInfo.Id,
|
UserId: userInfo.Id,
|
||||||
UserAgent: req.UserAgent,
|
UserAgent: req.UserAgent,
|
||||||
Identifier: req.Identifier,
|
Identifier: req.Identifier,
|
||||||
ShortCode: req.ShortCode,
|
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Online: false,
|
Online: false,
|
||||||
}
|
}
|
||||||
@ -213,10 +196,8 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
|
|
||||||
// Activate trial if enabled
|
// Activate trial if enabled
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
if l.svcCtx.Config.Register.EnableTrial {
|
||||||
var trialErr error
|
if err := l.activeTrial(userInfo.Id, db); err != nil {
|
||||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, db)
|
return err
|
||||||
if trialErr != nil {
|
|
||||||
return trialErr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,25 +212,6 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache after transaction success
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
|
||||||
// Clear user subscription cache
|
|
||||||
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear subscription cache
|
|
||||||
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear all server cache
|
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("device registration completed successfully",
|
l.Infow("device registration completed successfully",
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
logger.Field("identifier", req.Identifier),
|
logger.Field("identifier", req.Identifier),
|
||||||
@ -282,7 +244,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
return userInfo, nil
|
return userInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscribe, error) {
|
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("failed to find trial subscription template",
|
l.Errorw("failed to find trial subscription template",
|
||||||
@ -290,7 +252,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscri
|
|||||||
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@ -317,7 +279,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscri
|
|||||||
logger.Field("user_id", userId),
|
logger.Field("user_id", userId),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infow("trial subscription activated successfully",
|
l.Infow("trial subscription activated successfully",
|
||||||
@ -327,5 +289,5 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscri
|
|||||||
logger.Field("traffic", sub.Traffic),
|
logger.Field("traffic", sub.Traffic),
|
||||||
)
|
)
|
||||||
|
|
||||||
return userSub, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -341,7 +341,6 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userInfo *user.User
|
var userInfo *user.User
|
||||||
var trialSubscribe *user.Subscribe
|
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
if email != "" {
|
if email != "" {
|
||||||
l.Debugw("checking if email already exists",
|
l.Debugw("checking if email already exists",
|
||||||
@ -398,10 +397,8 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
|||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
)
|
)
|
||||||
var trialErr error
|
if err := l.activeTrial(userInfo.Id, requestID); err != nil {
|
||||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, requestID)
|
return err
|
||||||
if trialErr != nil {
|
|
||||||
return trialErr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,25 +415,6 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
|||||||
return userInfo, err
|
return userInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cache after transaction success
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
|
||||||
// Clear user subscription cache
|
|
||||||
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear subscription cache
|
|
||||||
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear all server cache
|
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("user registration completed successfully",
|
l.Infow("user registration completed successfully",
|
||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
@ -815,7 +793,7 @@ func (l *OAuthLoginGetTokenLogic) findOrRegisterUser(authType, openID, email, av
|
|||||||
return userInfo, nil
|
return userInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*user.Subscribe, error) {
|
func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error {
|
||||||
l.Debugw("fetching trial subscription template",
|
l.Debugw("fetching trial subscription template",
|
||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", uid),
|
logger.Field("user_id", uid),
|
||||||
@ -830,7 +808,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
|
|||||||
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@ -870,7 +848,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
|
|||||||
logger.Field("user_id", uid),
|
logger.Field("user_id", uid),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infow("trial subscription activated successfully",
|
l.Infow("trial subscription activated successfully",
|
||||||
@ -880,5 +858,5 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
|
|||||||
logger.Field("expire_time", expireTime),
|
logger.Field("expire_time", expireTime),
|
||||||
logger.Field("traffic", sub.Traffic),
|
logger.Field("traffic", sub.Traffic),
|
||||||
)
|
)
|
||||||
return userSub, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,65 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"go.uber.org/zap"
|
|
||||||
)
|
|
||||||
|
|
||||||
func registerIpLimit(svcCtx *svc.ServiceContext, ctx context.Context, registerIp, authType, account string) (isOk bool) {
|
|
||||||
if !svcCtx.Config.Register.EnableIpRegisterLimit {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a sorted set to track IP registrations with timestamp as score
|
|
||||||
// Key format: register:ip:{ip}
|
|
||||||
key := fmt.Sprintf("%s%s", config.RegisterIpKeyPrefix, registerIp)
|
|
||||||
now := time.Now().Unix()
|
|
||||||
expiration := int64(svcCtx.Config.Register.IpRegisterLimitDuration) * 60
|
|
||||||
|
|
||||||
// Clean up expired entries first (remove entries older than expiration duration)
|
|
||||||
expireTimestamp := now - expiration
|
|
||||||
removed, err := svcCtx.Redis.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", expireTimestamp)).Result()
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorf("[registerIpLimit] ZRemRangeByScore Err: %v", err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if removed > 0 {
|
|
||||||
zap.S().Debugf("[registerIpLimit] Cleaned %d expired entries for IP: %s", removed, registerIp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current count of registrations within the time window
|
|
||||||
count, err := svcCtx.Redis.ZCard(ctx, key).Result()
|
|
||||||
if err != nil {
|
|
||||||
zap.S().Errorf("[registerIpLimit] ZCard Err: %v", err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if limit is reached
|
|
||||||
if count >= svcCtx.Config.Register.IpRegisterLimit {
|
|
||||||
zap.S().Warnf("[registerIpLimit] IP %s exceeded limit: %d/%d", registerIp, count, svcCtx.Config.Register.IpRegisterLimit)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new registration entry with current timestamp as score
|
|
||||||
member := fmt.Sprintf("%s:%s", authType, account)
|
|
||||||
if err := svcCtx.Redis.ZAdd(ctx, key, redis.Z{
|
|
||||||
Score: float64(now),
|
|
||||||
Member: member,
|
|
||||||
}).Err(); err != nil {
|
|
||||||
zap.S().Errorf("[registerIpLimit] ZAdd Err: %v", err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set expiration on the sorted set key
|
|
||||||
if err := svcCtx.Redis.Expire(ctx, key, time.Minute*time.Duration(svcCtx.Config.Register.IpRegisterLimitDuration)).Err(); err != nil {
|
|
||||||
zap.S().Errorf("[registerIpLimit] Expire Err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
@ -121,8 +121,8 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
|||||||
// Don't fail register if device binding fails, just log the error
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -133,8 +133,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("identifier", req.Identifier),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/perfect-panel/server/internal/logic/common"
|
"github.com/perfect-panel/server/internal/logic/common"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
@ -47,22 +48,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
||||||
}
|
}
|
||||||
loginStatus := false
|
loginStatus := false
|
||||||
|
var userInfo *user.User
|
||||||
authMethodInfo, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber)
|
|
||||||
if err != nil {
|
|
||||||
if errors.As(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, authMethodInfo.UserId)
|
|
||||||
if err != nil {
|
|
||||||
if errors.As(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
// Record login status
|
// Record login status
|
||||||
defer func(svcCtx *svc.ServiceContext) {
|
defer func(svcCtx *svc.ServiceContext) {
|
||||||
if userInfo.Id != 0 {
|
if userInfo.Id != 0 {
|
||||||
@ -90,6 +76,22 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
}
|
}
|
||||||
}(l.svcCtx)
|
}(l.svcCtx)
|
||||||
|
|
||||||
|
authMethodInfo, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethodInfo.UserId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if req.Password == "" && req.TelephoneCode == "" {
|
if req.Password == "" && req.TelephoneCode == "" {
|
||||||
return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty")
|
return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty")
|
||||||
}
|
}
|
||||||
@ -135,8 +137,8 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate session id
|
// Generate session id
|
||||||
@ -148,8 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("identifier", req.Identifier),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -44,32 +43,19 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !l.svcCtx.Config.Mobile.Enable {
|
if l.svcCtx.Config.Mobile.Enable {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the email verification is enabled, the verification code is required
|
// if the email verification is enabled, the verification code is required
|
||||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(uint8(constant.Security)), phoneNumber)
|
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber)
|
||||||
l.Logger.Infof("TelephoneResetPassword cacheKey: %s, code: %s", cacheKey, code)
|
|
||||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
l.Logger.Infof("TelephoneResetPassword cacheKey: %s, code: %s,value : %s", cacheKey, code, value)
|
|
||||||
if value == "" {
|
|
||||||
l.Errorf("TelephoneResetPassword value empty: %s", value)
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload CacheKeyPayload
|
if value != code {
|
||||||
if err := json.Unmarshal([]byte(value), &payload); err != nil {
|
|
||||||
l.Errorf("TelephoneResetPassword Unmarshal Error: %s", err.Error())
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
|
||||||
}
|
|
||||||
|
|
||||||
if payload.Code != code {
|
|
||||||
l.Errorf("TelephoneResetPassword code: %s, code: %s", code, payload.Code)
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +96,8 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
|||||||
// Don't fail register if device binding fails, just log the error
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -122,8 +108,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("identifier", req.Identifier),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -45,7 +45,6 @@ func NewTelephoneUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceConte
|
|||||||
|
|
||||||
func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
|
func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
|
||||||
c := l.svcCtx.Config.Register
|
c := l.svcCtx.Config.Register
|
||||||
var trialSubscribe *user.Subscribe
|
|
||||||
// Check if the registration is stopped
|
// Check if the registration is stopped
|
||||||
if c.StopRegister {
|
if c.StopRegister {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
||||||
@ -103,9 +102,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "mobile", phoneNumber) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
|
|
||||||
}
|
|
||||||
// Generate password
|
// Generate password
|
||||||
pwd := tool.EncodePassWord(req.Password)
|
pwd := tool.EncodePassWord(req.Password)
|
||||||
userInfo := &user.User{
|
userInfo := &user.User{
|
||||||
@ -136,36 +133,12 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
}
|
}
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
if l.svcCtx.Config.Register.EnableTrial {
|
||||||
// Active trial
|
// Active trial
|
||||||
var trialErr error
|
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
|
return err
|
||||||
if trialErr != nil {
|
|
||||||
return trialErr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear cache after transaction success
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
|
||||||
// Clear user subscription cache
|
|
||||||
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear subscription cache
|
|
||||||
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear all server cache
|
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
@ -179,8 +152,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
// Don't fail register if device binding fails, just log the error
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -191,8 +164,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("identifier", req.Identifier),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
@ -254,10 +226,10 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
|
func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) error {
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
userSub := &user.Subscribe{
|
userSub := &user.Subscribe{
|
||||||
Id: 0,
|
Id: 0,
|
||||||
@ -273,10 +245,5 @@ func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, er
|
|||||||
UUID: uuidx.NewUUID().String(),
|
UUID: uuidx.NewUUID().String(),
|
||||||
Status: 1,
|
Status: 1,
|
||||||
}
|
}
|
||||||
err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
|
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return userSub, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -97,8 +97,8 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
// Don't fail login if device binding fails, just log the error
|
// Don't fail login if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -109,8 +109,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("identifier", req.Identifier),
|
jwt.WithOption("LoginType", req.LoginType),
|
||||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -42,7 +42,6 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
c := l.svcCtx.Config.Register
|
c := l.svcCtx.Config.Register
|
||||||
email := l.svcCtx.Config.Email
|
email := l.svcCtx.Config.Email
|
||||||
var referer *user.User
|
var referer *user.User
|
||||||
var trialSubscribe *user.Subscribe
|
|
||||||
// Check if the registration is stopped
|
// Check if the registration is stopped
|
||||||
if c.StopRegister {
|
if c.StopRegister {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
||||||
@ -90,10 +89,6 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserDisabled), "user email deleted: %v", req.Email)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserDisabled), "user email deleted: %v", req.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "email", req.Email) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate password
|
// Generate password
|
||||||
pwd := tool.EncodePassWord(req.Password)
|
pwd := tool.EncodePassWord(req.Password)
|
||||||
userInfo := &user.User{
|
userInfo := &user.User{
|
||||||
@ -128,36 +123,12 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
if l.svcCtx.Config.Register.EnableTrial {
|
||||||
// Active trial
|
// Active trial
|
||||||
var trialErr error
|
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
|
return err
|
||||||
if trialErr != nil {
|
|
||||||
return trialErr
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear cache after transaction success
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
|
||||||
// Clear user subscription cache
|
|
||||||
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear subscription cache
|
|
||||||
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
|
||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
// Clear all server cache
|
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
// Don't return error, just log it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||||
@ -170,8 +141,8 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
// Don't fail register if device binding fails, just log the error
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
if l.ctx.Value(constant.LoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -245,10 +216,10 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *UserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
|
func (l *UserRegisterLogic) activeTrial(uid int64) error {
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
userSub := &user.Subscribe{
|
userSub := &user.Subscribe{
|
||||||
UserId: uid,
|
UserId: uid,
|
||||||
@ -263,8 +234,5 @@ func (l *UserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
|
|||||||
UUID: uuidx.NewUUID().String(),
|
UUID: uuidx.NewUUID().String(),
|
||||||
Status: 1,
|
Status: 1,
|
||||||
}
|
}
|
||||||
if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil {
|
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return userSub, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,7 +82,7 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
|
|||||||
l.Logger.Error("[AlipayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[AlipayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
|
||||||
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -84,7 +84,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
|||||||
l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
||||||
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -85,7 +85,7 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
|
|||||||
l.Errorw("[StripeNotify] Marshal error", logger.Field("errors", err.Error()), logger.Field("payload", payload))
|
l.Errorw("[StripeNotify] Marshal error", logger.Field("errors", err.Error()), logger.Field("payload", payload))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
|
||||||
_, err = l.svcCtx.Queue.Enqueue(task)
|
_, err = l.svcCtx.Queue.Enqueue(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
||||||
|
|||||||
@ -55,25 +55,6 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
|
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// check subscribe plan quota limit
|
|
||||||
if sub.Quota > 0 {
|
|
||||||
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
|
|
||||||
}
|
|
||||||
var count int64
|
|
||||||
for _, v := range userSub {
|
|
||||||
if v.SubscribeId == req.SubscribeId {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count >= sub.Quota {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var discount float64 = 1
|
var discount float64 = 1
|
||||||
if sub.Discount != "" {
|
if sub.Discount != "" {
|
||||||
var dis []types.SubscribeDiscount
|
var dis []types.SubscribeDiscount
|
||||||
@ -117,6 +98,18 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
couponAmount = calculateCoupon(amount, couponInfo)
|
couponAmount = calculateCoupon(amount, couponInfo)
|
||||||
}
|
}
|
||||||
amount -= couponAmount
|
amount -= couponAmount
|
||||||
|
|
||||||
|
var deductionAmount int64
|
||||||
|
// Check user deduction amount
|
||||||
|
if u.GiftAmount > 0 {
|
||||||
|
if u.GiftAmount >= amount {
|
||||||
|
deductionAmount = amount
|
||||||
|
amount = 0
|
||||||
|
} else {
|
||||||
|
deductionAmount = u.GiftAmount
|
||||||
|
amount -= u.GiftAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
var feeAmount int64
|
var feeAmount int64
|
||||||
if req.Payment != 0 {
|
if req.Payment != 0 {
|
||||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
@ -127,19 +120,8 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
// Calculate the handling fee
|
// Calculate the handling fee
|
||||||
if amount > 0 {
|
if amount > 0 {
|
||||||
feeAmount = calculateFee(amount, payment)
|
feeAmount = calculateFee(amount, payment)
|
||||||
amount += feeAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Calculate gift amount deduction after fee calculation
|
|
||||||
var deductionAmount int64
|
|
||||||
if u.GiftAmount > 0 && amount > 0 {
|
|
||||||
if u.GiftAmount >= amount {
|
|
||||||
deductionAmount = amount
|
|
||||||
amount = 0
|
|
||||||
} else {
|
|
||||||
deductionAmount = u.GiftAmount
|
|
||||||
amount -= u.GiftAmount
|
|
||||||
}
|
}
|
||||||
|
amount += feeAmount
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = &types.PreOrderResponse{
|
resp = &types.PreOrderResponse{
|
||||||
|
|||||||
@ -93,6 +93,19 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check subscribe plan limit
|
||||||
|
if sub.Quota > 0 {
|
||||||
|
var count int64
|
||||||
|
for _, v := range userSub {
|
||||||
|
if v.SubscribeId == req.SubscribeId {
|
||||||
|
count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count >= sub.Quota {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var discount float64 = 1
|
var discount float64 = 1
|
||||||
if sub.Discount != "" {
|
if sub.Discount != "" {
|
||||||
var dis []types.SubscribeDiscount
|
var dis []types.SubscribeDiscount
|
||||||
@ -147,6 +160,19 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
// Calculate the handling fee
|
// Calculate the handling fee
|
||||||
amount -= coupon
|
amount -= coupon
|
||||||
|
var deductionAmount int64
|
||||||
|
// Check user deduction amount
|
||||||
|
if u.GiftAmount > 0 {
|
||||||
|
if u.GiftAmount >= amount {
|
||||||
|
deductionAmount = amount
|
||||||
|
amount = 0
|
||||||
|
u.GiftAmount -= deductionAmount
|
||||||
|
} else {
|
||||||
|
deductionAmount = u.GiftAmount
|
||||||
|
amount -= u.GiftAmount
|
||||||
|
u.GiftAmount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
// find payment method
|
// find payment method
|
||||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -168,17 +194,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Calculate gift amount deduction after fee calculation
|
|
||||||
var deductionAmount int64
|
|
||||||
if u.GiftAmount > 0 && amount > 0 {
|
|
||||||
if u.GiftAmount >= amount {
|
|
||||||
deductionAmount = amount
|
|
||||||
amount = 0
|
|
||||||
} else {
|
|
||||||
deductionAmount = u.GiftAmount
|
|
||||||
amount -= u.GiftAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// query user is new purchase or renewal
|
// query user is new purchase or renewal
|
||||||
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -206,28 +221,9 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
// Database transaction
|
// Database transaction
|
||||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||||
// check subscribe plan quota limit inside transaction to prevent race condition
|
// update user deduction && Pre deduction ,Return after canceling the order
|
||||||
if sub.Quota > 0 {
|
|
||||||
var currentUserSub []user.Subscribe
|
|
||||||
if e := db.Model(&user.Subscribe{}).Where("user_id = ?", u.Id).Find(¤tUserSub).Error; e != nil {
|
|
||||||
l.Errorw("[Purchase] Database query error", logger.Field("error", e.Error()), logger.Field("user_id", u.Id))
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
var count int64
|
|
||||||
for _, v := range currentUserSub {
|
|
||||||
if v.SubscribeId == req.SubscribeId {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count >= sub.Quota {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// update user gift amount and create deduction record
|
|
||||||
if orderInfo.GiftAmount > 0 {
|
if orderInfo.GiftAmount > 0 {
|
||||||
// deduct gift amount from user
|
// update user deduction && Pre deduction ,Return after canceling the order
|
||||||
u.GiftAmount -= orderInfo.GiftAmount
|
|
||||||
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil {
|
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil {
|
||||||
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
|
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
|
||||||
return e
|
return e
|
||||||
|
|||||||
@ -366,7 +366,6 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
|
|||||||
}
|
}
|
||||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create payment URL for user redirection
|
// Create payment URL for user redirection
|
||||||
url := client.CreatePayUrl(epay.Order{
|
url := client.CreatePayUrl(epay.Order{
|
||||||
Name: l.svcCtx.Config.Site.SiteName,
|
Name: l.svcCtx.Config.Site.SiteName,
|
||||||
|
|||||||
@ -150,7 +150,7 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
|
|||||||
}
|
}
|
||||||
content, _ := tempOrder.Marshal()
|
content, _ := tempOrder.Marshal()
|
||||||
|
|
||||||
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), 24*time.Hour).Result(); err != nil {
|
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), CloseOrderTimeMinutes*time.Minute).Result(); err != nil {
|
||||||
l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo))
|
l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,224 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hibiken/asynq"
|
|
||||||
"github.com/perfect-panel/server/internal/model/order"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
queue "github.com/perfect-panel/server/queue/types"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RedeemCodeLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redeem code
|
|
||||||
func NewRedeemCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RedeemCodeLogic {
|
|
||||||
return &RedeemCodeLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *RedeemCodeLogic) RedeemCode(req *types.RedeemCodeRequest) (resp *types.RedeemCodeResponse, err error) {
|
|
||||||
// Get user from context
|
|
||||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
|
||||||
if !ok {
|
|
||||||
logger.Error("current user is not found in context")
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用Redis分布式锁防止并发重复兑换
|
|
||||||
lockKey := fmt.Sprintf("redemption_lock:%d:%s", u.Id, req.Code)
|
|
||||||
lockSuccess, err := l.svcCtx.Redis.SetNX(l.ctx, lockKey, "1", 10*time.Second).Result()
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Acquire lock failed", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "system busy, please try again later")
|
|
||||||
}
|
|
||||||
if !lockSuccess {
|
|
||||||
l.Errorw("[RedeemCode] Redemption in progress",
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("code", req.Code))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "redemption in progress, please wait")
|
|
||||||
}
|
|
||||||
defer l.svcCtx.Redis.Del(l.ctx, lockKey)
|
|
||||||
|
|
||||||
// Find redemption code by code
|
|
||||||
redemptionCode, err := l.svcCtx.RedemptionCodeModel.FindOneByCode(l.ctx, req.Code)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
l.Errorw("[RedeemCode] Redemption code not found", logger.Field("code", req.Code))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code not found")
|
|
||||||
}
|
|
||||||
l.Errorw("[RedeemCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if redemption code is enabled
|
|
||||||
if redemptionCode.Status != 1 {
|
|
||||||
l.Errorw("[RedeemCode] Redemption code is disabled",
|
|
||||||
logger.Field("code", req.Code),
|
|
||||||
logger.Field("status", redemptionCode.Status))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if redemption code has remaining count
|
|
||||||
if redemptionCode.TotalCount > 0 && redemptionCode.UsedCount >= redemptionCode.TotalCount {
|
|
||||||
l.Errorw("[RedeemCode] Redemption code has been fully used",
|
|
||||||
logger.Field("code", req.Code),
|
|
||||||
logger.Field("total_count", redemptionCode.TotalCount),
|
|
||||||
logger.Field("used_count", redemptionCode.UsedCount))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code has been fully used")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user has already redeemed this code
|
|
||||||
userRecords, err := l.svcCtx.RedemptionRecordModel.FindByUserId(l.ctx, u.Id)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Database Error", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption records error: %v", err.Error())
|
|
||||||
}
|
|
||||||
for _, record := range userRecords {
|
|
||||||
if record.RedemptionCodeId == redemptionCode.Id {
|
|
||||||
l.Errorw("[RedeemCode] User has already redeemed this code",
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("code", req.Code))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "you have already redeemed this code")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find subscribe plan from redemption code
|
|
||||||
subscribePlan, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, redemptionCode.SubscribePlan)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Subscribe plan not found",
|
|
||||||
logger.Field("subscribe_plan", redemptionCode.SubscribePlan),
|
|
||||||
logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "subscribe plan not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if subscribe plan is available
|
|
||||||
if !*subscribePlan.Sell {
|
|
||||||
l.Errorw("[RedeemCode] Subscribe plan is not available",
|
|
||||||
logger.Field("subscribe_plan", redemptionCode.SubscribePlan))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe plan is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查配额限制(预检查,队列任务中会再次检查)
|
|
||||||
if subscribePlan.Quota > 0 {
|
|
||||||
var count int64
|
|
||||||
err = l.svcCtx.DB.Model(&user.Subscribe{}).
|
|
||||||
Where("user_id = ? AND subscribe_id = ?", u.Id, redemptionCode.SubscribePlan).
|
|
||||||
Count(&count).Error
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Check quota failed", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check quota failed")
|
|
||||||
}
|
|
||||||
if count >= subscribePlan.Quota {
|
|
||||||
l.Errorw("[RedeemCode] Subscribe quota limit exceeded",
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
logger.Field("subscribe_id", redemptionCode.SubscribePlan),
|
|
||||||
logger.Field("quota", subscribePlan.Quota),
|
|
||||||
logger.Field("current_count", count))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "subscribe quota limit exceeded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否首次购买
|
|
||||||
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Check user order failed", logger.Field("error", err.Error()))
|
|
||||||
// 可以继续,默认为false
|
|
||||||
isNew = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建Order记录
|
|
||||||
orderInfo := &order.Order{
|
|
||||||
UserId: u.Id,
|
|
||||||
OrderNo: tool.GenerateTradeNo(),
|
|
||||||
Type: 5, // 兑换类型
|
|
||||||
Quantity: redemptionCode.Quantity,
|
|
||||||
Price: 0, // 兑换无价格
|
|
||||||
Amount: 0, // 兑换无金额
|
|
||||||
Discount: 0,
|
|
||||||
GiftAmount: 0,
|
|
||||||
Coupon: "",
|
|
||||||
CouponDiscount: 0,
|
|
||||||
PaymentId: 0,
|
|
||||||
Method: "redemption",
|
|
||||||
FeeAmount: 0,
|
|
||||||
Commission: 0,
|
|
||||||
Status: 2, // 直接设置为已支付
|
|
||||||
SubscribeId: redemptionCode.SubscribePlan,
|
|
||||||
IsNew: isNew,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存Order到数据库
|
|
||||||
err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Create order failed", logger.Field("error", err.Error()))
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create order failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存兑换码信息到Redis(供队列任务使用)
|
|
||||||
cacheKey := fmt.Sprintf("redemption_order:%s", orderInfo.OrderNo)
|
|
||||||
cacheData := map[string]interface{}{
|
|
||||||
"redemption_code_id": redemptionCode.Id,
|
|
||||||
"unit_time": redemptionCode.UnitTime,
|
|
||||||
"quantity": redemptionCode.Quantity,
|
|
||||||
}
|
|
||||||
jsonData, _ := json.Marshal(cacheData)
|
|
||||||
err = l.svcCtx.Redis.Set(l.ctx, cacheKey, jsonData, 2*time.Hour).Err()
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Cache redemption data failed", logger.Field("error", err.Error()))
|
|
||||||
// 缓存失败,删除已创建的Order避免孤儿记录
|
|
||||||
if delErr := l.svcCtx.OrderModel.Delete(l.ctx, orderInfo.Id); delErr != nil {
|
|
||||||
l.Errorw("[RedeemCode] Delete order failed after cache error",
|
|
||||||
logger.Field("order_id", orderInfo.Id),
|
|
||||||
logger.Field("error", delErr.Error()))
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "cache redemption data failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 触发队列任务
|
|
||||||
payload := queue.ForthwithActivateOrderPayload{
|
|
||||||
OrderNo: orderInfo.OrderNo,
|
|
||||||
}
|
|
||||||
bytes, _ := json.Marshal(&payload)
|
|
||||||
task := asynq.NewTask(queue.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
|
||||||
_, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("[RedeemCode] Enqueue task failed", logger.Field("error", err.Error()))
|
|
||||||
// 入队失败,删除Order和Redis缓存
|
|
||||||
l.svcCtx.Redis.Del(l.ctx, cacheKey)
|
|
||||||
if delErr := l.svcCtx.OrderModel.Delete(l.ctx, orderInfo.Id); delErr != nil {
|
|
||||||
l.Errorw("[RedeemCode] Delete order failed after enqueue error",
|
|
||||||
logger.Field("order_id", orderInfo.Id),
|
|
||||||
logger.Field("error", delErr.Error()))
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enqueue task failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("[RedeemCode] Redemption order created successfully",
|
|
||||||
logger.Field("order_no", orderInfo.OrderNo),
|
|
||||||
logger.Field("user_id", u.Id),
|
|
||||||
)
|
|
||||||
|
|
||||||
return &types.RedeemCodeResponse{
|
|
||||||
Message: "Redemption successful, processing...",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@ -38,7 +38,7 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
|
|
||||||
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 0, 1, 2, 3)
|
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR")
|
||||||
@ -79,6 +79,7 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
|
|||||||
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
|
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
|
||||||
userSubscribeInfo.IsTryOut = true
|
userSubscribeInfo.IsTryOut = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.List = append(resp.List, userSubscribeInfo)
|
resp.List = append(resp.List, userSubscribeInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,21 +137,16 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
||||||
Id: n.Id,
|
Id: n.Id,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
Uuid: userSub.UUID,
|
Uuid: userSub.UUID,
|
||||||
Protocol: n.Protocol,
|
Protocol: n.Protocol,
|
||||||
Protocols: server.Protocols,
|
Port: n.Port,
|
||||||
Port: n.Port,
|
Address: n.Address,
|
||||||
Address: n.Address,
|
Tags: strings.Split(n.Tags, ","),
|
||||||
Tags: strings.Split(n.Tags, ","),
|
Country: server.Country,
|
||||||
Country: server.Country,
|
City: server.City,
|
||||||
City: server.City,
|
CreatedAt: n.CreatedAt.Unix(),
|
||||||
Latitude: server.Latitude,
|
|
||||||
Longitude: server.Longitude,
|
|
||||||
LongitudeCenter: server.LongitudeCenter,
|
|
||||||
LatitudeCenter: server.LatitudeCenter,
|
|
||||||
CreatedAt: n.CreatedAt.Unix(),
|
|
||||||
}
|
}
|
||||||
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
|
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,86 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeleteCurrentUserAccountLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete Current User Account
|
|
||||||
func NewDeleteCurrentUserAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCurrentUserAccountLogic {
|
|
||||||
return &DeleteCurrentUserAccountLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *DeleteCurrentUserAccountLogic) DeleteCurrentUserAccount() (err error) {
|
|
||||||
userInfo, exists := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
|
||||||
if !exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, userInfo.Id)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("FindOne Error", logger.Field("error", err))
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth methods failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
|
||||||
//delete user devices
|
|
||||||
if len(userInfo.UserDevices) > 0 {
|
|
||||||
for _, device := range userInfo.UserDevices {
|
|
||||||
if err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id, tx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete user auth methods
|
|
||||||
if len(userInfo.AuthMethods) > 0 {
|
|
||||||
for _, authMethod := range userInfo.AuthMethods {
|
|
||||||
if err = l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, userInfo.Id, authMethod.AuthType); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete user subscribes
|
|
||||||
var subscribes []*user.SubscribeDetails
|
|
||||||
subscribes, err = l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, subscribe := range subscribes {
|
|
||||||
if err = l.svcCtx.UserModel.DeleteSubscribe(l.ctx, subscribe.Token, tx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// delete user account
|
|
||||||
return l.svcCtx.UserModel.BatchDeleteUser(l.ctx, []int64{userInfo.Id}, tx)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "find user auth methods failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, l.ctx.Value(constant.CtxKeySessionID))
|
|
||||||
if err = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err(); err != nil {
|
|
||||||
l.Logger.Errorf("delete session id cache failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/internal/types"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeviceOnlineStatisticsLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device Online Statistics
|
|
||||||
func NewDeviceOnlineStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceOnlineStatisticsLogic {
|
|
||||||
return &DeviceOnlineStatisticsLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *DeviceOnlineStatisticsLogic) DeviceOnlineStatistics() (resp *types.GetDeviceOnlineStatsResponse, err error) {
|
|
||||||
u := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
|
||||||
//获取历史最长在线时间
|
|
||||||
var OnlineSeconds int64
|
|
||||||
if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("online_seconds").Order("online_seconds desc").Limit(1).Scan(&OnlineSeconds).Error; err != nil {
|
|
||||||
l.Logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取历史连续最长在线天数
|
|
||||||
var DurationDays int64
|
|
||||||
if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("duration_days").Order("duration_days desc").Limit(1).Scan(&DurationDays).Error; err != nil {
|
|
||||||
l.Logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取近七天在线情况
|
|
||||||
var userOnlineRecord []user.DeviceOnlineRecord
|
|
||||||
if err := l.svcCtx.DB.Model(&userOnlineRecord).Where("user_id = ? and created_at >= ?", u.Id, time.Now().AddDate(0, 0, -7).Format(time.DateTime)).Order("created_at desc").Find(&userOnlineRecord).Error; err != nil {
|
|
||||||
l.Logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取当前连续在线天数
|
|
||||||
var currentContinuousDays int64
|
|
||||||
if len(userOnlineRecord) > 0 {
|
|
||||||
currentContinuousDays = userOnlineRecord[0].DurationDays
|
|
||||||
} else {
|
|
||||||
currentContinuousDays = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var dates []string
|
|
||||||
for i := 0; i < 7; i++ {
|
|
||||||
date := time.Now().AddDate(0, 0, -i).Format(time.DateOnly)
|
|
||||||
dates = append(dates, date)
|
|
||||||
}
|
|
||||||
|
|
||||||
onlineDays := make(map[string]types.WeeklyStat)
|
|
||||||
for _, record := range userOnlineRecord {
|
|
||||||
//获取近七天在线情况
|
|
||||||
onlineTime := record.OnlineTime.Format(time.DateOnly)
|
|
||||||
if weeklyStat, ok := onlineDays[onlineTime]; ok {
|
|
||||||
weeklyStat.Hours += float64(record.OnlineSeconds)
|
|
||||||
onlineDays[onlineTime] = weeklyStat
|
|
||||||
} else {
|
|
||||||
onlineDays[onlineTime] = types.WeeklyStat{
|
|
||||||
Hours: float64(record.OnlineSeconds),
|
|
||||||
//根据日期获取周几
|
|
||||||
DayName: record.OnlineTime.Weekday().String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//补全不存在的日期
|
|
||||||
for _, date := range dates {
|
|
||||||
if _, ok := onlineDays[date]; !ok {
|
|
||||||
onlineTime, _ := time.Parse(time.DateOnly, date)
|
|
||||||
onlineDays[date] = types.WeeklyStat{
|
|
||||||
DayName: onlineTime.Weekday().String(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var keys []string
|
|
||||||
for key := range onlineDays {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
//排序
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
var weeklyStats []types.WeeklyStat
|
|
||||||
for index, key := range keys {
|
|
||||||
weeklyStat := onlineDays[key]
|
|
||||||
weeklyStat.Day = index + 1
|
|
||||||
weeklyStat.Hours = weeklyStat.Hours / float64(3600)
|
|
||||||
weeklyStats = append(weeklyStats, weeklyStat)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp = &types.GetDeviceOnlineStatsResponse{
|
|
||||||
WeeklyStats: weeklyStats,
|
|
||||||
ConnectionRecords: types.ConnectionRecords{
|
|
||||||
CurrentContinuousDays: currentContinuousDays,
|
|
||||||
HistoryContinuousDays: DurationDays,
|
|
||||||
LongestSingleConnection: OnlineSeconds / 60,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@ -83,9 +83,6 @@ func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetU
|
|||||||
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
|
||||||
}
|
}
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear server cache: %v", err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,23 +64,9 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
|
||||||
}
|
}
|
||||||
var count int64
|
sessionId := l.ctx.Value(constant.CtxKeySessionID)
|
||||||
err = tx.Model(user.AuthMethods{}).Where("user_id = ?", deleteDevice.UserId).Count(&count).Error
|
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||||
if err != nil {
|
l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count user auth methods err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if count < 1 {
|
|
||||||
_ = tx.Where("id = ?", deleteDevice.UserId).Delete(&user.User{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
//remove device cache
|
|
||||||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, deleteDevice.Identifier)
|
|
||||||
if sessionId, err := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); err == nil && sessionId != "" {
|
|
||||||
_ = l.svcCtx.Redis.Del(l.ctx, deviceCacheKey).Err()
|
|
||||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
|
||||||
_ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,87 +0,0 @@
|
|||||||
package ws
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
sysErr "errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DeviceWsConnectLogic struct {
|
|
||||||
logger.Logger
|
|
||||||
ctx context.Context
|
|
||||||
svcCtx *svc.ServiceContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// Webosocket Device Connect
|
|
||||||
func NewDeviceWsConnectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceWsConnectLogic {
|
|
||||||
return &DeviceWsConnectLogic{
|
|
||||||
Logger: logger.WithContext(ctx),
|
|
||||||
ctx: ctx,
|
|
||||||
svcCtx: svcCtx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *DeviceWsConnectLogic) DeviceWsConnect(c *gin.Context) error {
|
|
||||||
|
|
||||||
value := l.ctx.Value(constant.CtxKeyIdentifier)
|
|
||||||
if value == nil || value.(string) == "" {
|
|
||||||
value, _ = c.GetQuery("identifier")
|
|
||||||
if value == nil || value.(string) == "" {
|
|
||||||
l.Errorf("DeviceWsConnectLogic DeviceWsConnect identifier is empty")
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "identifier is empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
identifier := value.(string)
|
|
||||||
_, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
|
|
||||||
if err != nil && !sysErr.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
l.Errorf("DeviceWsConnectLogic DeviceWsConnect FindOneDeviceByIdentifier err: %v", err)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
value = l.ctx.Value(constant.CtxKeyUser)
|
|
||||||
if value == nil {
|
|
||||||
l.Errorf("DeviceWsConnectLogic DeviceWsConnect value is nil")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
userInfo := value.(*user.User)
|
|
||||||
if sysErr.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
device := user.Device{
|
|
||||||
Identifier: identifier,
|
|
||||||
UserId: userInfo.Id,
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
Online: true,
|
|
||||||
Enabled: true,
|
|
||||||
}
|
|
||||||
err := l.svcCtx.UserModel.InsertDevice(l.ctx, &device)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorf("DeviceWsConnectLogic DeviceWsConnect InsertDevice err: %v", err)
|
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//默认在线设备1
|
|
||||||
maxDevice := 3
|
|
||||||
subscribe, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2)
|
|
||||||
if err == nil {
|
|
||||||
for _, sub := range subscribe {
|
|
||||||
if time.Now().Before(sub.ExpireTime) {
|
|
||||||
deviceLimit := int(sub.Subscribe.DeviceLimit)
|
|
||||||
if deviceLimit > maxDevice {
|
|
||||||
maxDevice = deviceLimit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
l.svcCtx.DeviceManager.AddDevice(c.Writer, c.Request, l.ctx.Value(constant.CtxKeySessionID).(string), userInfo.Id, identifier, maxDevice)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@ -42,11 +42,8 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loginType := ""
|
loginType := ""
|
||||||
if claims["CtxLoginType"] != nil {
|
if claims["LoginType"] != nil {
|
||||||
loginType = claims["CtxLoginType"].(string)
|
loginType = claims["LoginType"].(string)
|
||||||
}
|
|
||||||
if claims["identifier"] != nil {
|
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeyIdentifier, claims["identifier"].(string))
|
|
||||||
}
|
}
|
||||||
// get user id from token
|
// get user id from token
|
||||||
userId := int64(claims["UserId"].(float64))
|
userId := int64(claims["UserId"].(float64))
|
||||||
@ -85,10 +82,9 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, constant.CtxLoginType, loginType)
|
ctx = context.WithValue(ctx, constant.LoginType, loginType)
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
|
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
||||||
|
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,11 +42,11 @@ func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
|
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
|
||||||
ctx = context.WithValue(ctx, constant.CtxLoginType, c.GetHeader("Login-Type"))
|
ctx = context.WithValue(ctx, constant.LoginType, c.GetHeader("Login-Type"))
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
loginType, ok := ctx.Value(constant.CtxLoginType).(string)
|
loginType, ok := ctx.Value(constant.LoginType).(string)
|
||||||
if !ok || loginType != "device" {
|
if !ok || loginType != "device" {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
|||||||
@ -27,9 +27,6 @@ type Filter struct {
|
|||||||
|
|
||||||
// GetAnnouncementListByPage get announcement list by page
|
// GetAnnouncementListByPage get announcement list by page
|
||||||
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
|
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
|
||||||
if size == 0 {
|
|
||||||
size = 10
|
|
||||||
}
|
|
||||||
var list []*Announcement
|
var list []*Announcement
|
||||||
var total int64
|
var total int64
|
||||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||||
|
|||||||
@ -13,7 +13,6 @@ type customServerLogicModel interface {
|
|||||||
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
|
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
|
||||||
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
|
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
|
||||||
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
|
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
|
||||||
ClearServerAllCache(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -172,30 +171,6 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
|
|
||||||
var cursor uint64
|
|
||||||
var keys []string
|
|
||||||
prefix := ServerUserListCacheKey + "*"
|
|
||||||
for {
|
|
||||||
scanKeys, newCursor, err := m.Cache.Scan(ctx, cursor, prefix, 999).Result()
|
|
||||||
if err != nil {
|
|
||||||
m.Logger.Error(ctx, fmt.Sprintf("ClearServerAllCache err:%v", err))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache query keys:%v", scanKeys))
|
|
||||||
keys = append(keys, scanKeys...)
|
|
||||||
cursor = newCursor
|
|
||||||
if cursor == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(keys) > 0 {
|
|
||||||
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache keys:%v", keys))
|
|
||||||
return m.Cache.Del(ctx, keys...).Err()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// InSet 支持多值 OR 查询
|
// InSet 支持多值 OR 查询
|
||||||
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
|||||||
@ -15,16 +15,12 @@ type Server struct {
|
|||||||
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
|
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
|
||||||
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
|
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
|
||||||
//Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
|
//Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
|
||||||
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
|
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
|
||||||
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
|
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
|
||||||
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
|
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
|
||||||
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
|
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
|
||||||
Longitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Longitude"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
Latitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Latitude"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
LongitudeCenter string `gorm:"type:varchar(50);not null;default:'0.0';comment:Center Longitude"`
|
|
||||||
LatitudeCenter string `gorm:"type:varchar(50);not null;default:'0.0';comment:Center Latitude"`
|
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Server) TableName() string {
|
func (*Server) TableName() string {
|
||||||
|
|||||||
@ -1,288 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/cache"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ RedemptionCodeModel = (*customRedemptionCodeModel)(nil)
|
|
||||||
var _ RedemptionRecordModel = (*customRedemptionRecordModel)(nil)
|
|
||||||
|
|
||||||
var (
|
|
||||||
cacheRedemptionCodeIdPrefix = "cache:redemption_code:id:"
|
|
||||||
cacheRedemptionCodeCodePrefix = "cache:redemption_code:code:"
|
|
||||||
cacheRedemptionRecordIdPrefix = "cache:redemption_record:id:"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
RedemptionCodeModel interface {
|
|
||||||
Insert(ctx context.Context, data *RedemptionCode) error
|
|
||||||
FindOne(ctx context.Context, id int64) (*RedemptionCode, error)
|
|
||||||
FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error)
|
|
||||||
Update(ctx context.Context, data *RedemptionCode, tx ...*gorm.DB) error
|
|
||||||
Delete(ctx context.Context, id int64) error
|
|
||||||
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
|
|
||||||
customRedemptionCodeLogicModel
|
|
||||||
}
|
|
||||||
|
|
||||||
RedemptionRecordModel interface {
|
|
||||||
Insert(ctx context.Context, data *RedemptionRecord, tx ...*gorm.DB) error
|
|
||||||
FindOne(ctx context.Context, id int64) (*RedemptionRecord, error)
|
|
||||||
Update(ctx context.Context, data *RedemptionRecord) error
|
|
||||||
Delete(ctx context.Context, id int64) error
|
|
||||||
customRedemptionRecordLogicModel
|
|
||||||
}
|
|
||||||
|
|
||||||
customRedemptionCodeLogicModel interface {
|
|
||||||
QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error)
|
|
||||||
BatchDelete(ctx context.Context, ids []int64) error
|
|
||||||
IncrementUsedCount(ctx context.Context, id int64, tx ...*gorm.DB) error
|
|
||||||
}
|
|
||||||
|
|
||||||
customRedemptionRecordLogicModel interface {
|
|
||||||
QueryRedemptionRecordListByPage(ctx context.Context, page, size int, userId int64, codeId int64) (total int64, list []*RedemptionRecord, err error)
|
|
||||||
FindByUserId(ctx context.Context, userId int64) ([]*RedemptionRecord, error)
|
|
||||||
FindByCodeId(ctx context.Context, codeId int64) ([]*RedemptionRecord, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
customRedemptionCodeModel struct {
|
|
||||||
*defaultRedemptionCodeModel
|
|
||||||
}
|
|
||||||
defaultRedemptionCodeModel struct {
|
|
||||||
cache.CachedConn
|
|
||||||
table string
|
|
||||||
}
|
|
||||||
|
|
||||||
customRedemptionRecordModel struct {
|
|
||||||
*defaultRedemptionRecordModel
|
|
||||||
}
|
|
||||||
defaultRedemptionRecordModel struct {
|
|
||||||
cache.CachedConn
|
|
||||||
table string
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func newRedemptionCodeModel(db *gorm.DB, c *redis.Client) *defaultRedemptionCodeModel {
|
|
||||||
return &defaultRedemptionCodeModel{
|
|
||||||
CachedConn: cache.NewConn(db, c),
|
|
||||||
table: "`redemption_code`",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRedemptionRecordModel(db *gorm.DB, c *redis.Client) *defaultRedemptionRecordModel {
|
|
||||||
return &defaultRedemptionRecordModel{
|
|
||||||
CachedConn: cache.NewConn(db, c),
|
|
||||||
table: "`redemption_record`",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedemptionCode cache methods
|
|
||||||
func (m *defaultRedemptionCodeModel) getCacheKeys(data *RedemptionCode) []string {
|
|
||||||
if data == nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
codeIdKey := fmt.Sprintf("%s%v", cacheRedemptionCodeIdPrefix, data.Id)
|
|
||||||
codeCodeKey := fmt.Sprintf("%s%v", cacheRedemptionCodeCodePrefix, data.Code)
|
|
||||||
cacheKeys := []string{
|
|
||||||
codeIdKey,
|
|
||||||
codeCodeKey,
|
|
||||||
}
|
|
||||||
return cacheKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionCodeModel) Insert(ctx context.Context, data *RedemptionCode) error {
|
|
||||||
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
|
||||||
return conn.Create(data).Error
|
|
||||||
}, m.getCacheKeys(data)...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionCodeModel) FindOne(ctx context.Context, id int64) (*RedemptionCode, error) {
|
|
||||||
codeIdKey := fmt.Sprintf("%s%v", cacheRedemptionCodeIdPrefix, id)
|
|
||||||
var resp RedemptionCode
|
|
||||||
err := m.QueryCtx(ctx, &resp, codeIdKey, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
return conn.Model(&RedemptionCode{}).Where("`id` = ?", id).First(&resp).Error
|
|
||||||
})
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return &resp, nil
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionCodeModel) FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error) {
|
|
||||||
codeCodeKey := fmt.Sprintf("%s%v", cacheRedemptionCodeCodePrefix, code)
|
|
||||||
var resp RedemptionCode
|
|
||||||
err := m.QueryCtx(ctx, &resp, codeCodeKey, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
return conn.Model(&RedemptionCode{}).Where("`code` = ?", code).First(&resp).Error
|
|
||||||
})
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return &resp, nil
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionCodeModel) Update(ctx context.Context, data *RedemptionCode, tx ...*gorm.DB) error {
|
|
||||||
old, err := m.FindOne(ctx, data.Id)
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
|
||||||
db := conn
|
|
||||||
if len(tx) > 0 {
|
|
||||||
db = tx[0]
|
|
||||||
}
|
|
||||||
return db.Save(data).Error
|
|
||||||
}, m.getCacheKeys(old)...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionCodeModel) Delete(ctx context.Context, id int64) error {
|
|
||||||
data, err := m.FindOne(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
|
||||||
db := conn
|
|
||||||
return db.Delete(&RedemptionCode{}, id).Error
|
|
||||||
}, m.getCacheKeys(data)...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionCodeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
|
|
||||||
return m.TransactCtx(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedemptionCode custom logic methods
|
|
||||||
func (m *customRedemptionCodeModel) QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error) {
|
|
||||||
err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
db := conn.Model(&RedemptionCode{})
|
|
||||||
if subscribePlan != 0 {
|
|
||||||
db = db.Where("subscribe_plan = ?", subscribePlan)
|
|
||||||
}
|
|
||||||
if unitTime != "" {
|
|
||||||
db = db.Where("unit_time = ?", unitTime)
|
|
||||||
}
|
|
||||||
if code != "" {
|
|
||||||
db = db.Where("code like ?", "%"+code+"%")
|
|
||||||
}
|
|
||||||
return db.Count(&total).Limit(size).Offset((page - 1) * size).Order("created_at DESC").Find(v).Error
|
|
||||||
})
|
|
||||||
return total, list, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *customRedemptionCodeModel) BatchDelete(ctx context.Context, ids []int64) error {
|
|
||||||
var err error
|
|
||||||
for _, id := range ids {
|
|
||||||
if err = m.Delete(ctx, id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *customRedemptionCodeModel) IncrementUsedCount(ctx context.Context, id int64, tx ...*gorm.DB) error {
|
|
||||||
data, err := m.FindOne(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data.UsedCount++
|
|
||||||
if len(tx) > 0 {
|
|
||||||
return m.Update(ctx, data, tx[0])
|
|
||||||
}
|
|
||||||
return m.Update(ctx, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedemptionRecord cache methods
|
|
||||||
func (m *defaultRedemptionRecordModel) getCacheKeys(data *RedemptionRecord) []string {
|
|
||||||
if data == nil {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
recordIdKey := fmt.Sprintf("%s%v", cacheRedemptionRecordIdPrefix, data.Id)
|
|
||||||
cacheKeys := []string{
|
|
||||||
recordIdKey,
|
|
||||||
}
|
|
||||||
return cacheKeys
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionRecordModel) Insert(ctx context.Context, data *RedemptionRecord, tx ...*gorm.DB) error {
|
|
||||||
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
|
||||||
if len(tx) > 0 {
|
|
||||||
conn = tx[0]
|
|
||||||
}
|
|
||||||
return conn.Create(data).Error
|
|
||||||
}, m.getCacheKeys(data)...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionRecordModel) FindOne(ctx context.Context, id int64) (*RedemptionRecord, error) {
|
|
||||||
recordIdKey := fmt.Sprintf("%s%v", cacheRedemptionRecordIdPrefix, id)
|
|
||||||
var resp RedemptionRecord
|
|
||||||
err := m.QueryCtx(ctx, &resp, recordIdKey, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
return conn.Model(&RedemptionRecord{}).Where("`id` = ?", id).First(&resp).Error
|
|
||||||
})
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
return &resp, nil
|
|
||||||
default:
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionRecordModel) Update(ctx context.Context, data *RedemptionRecord) error {
|
|
||||||
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
|
||||||
db := conn
|
|
||||||
return db.Save(data).Error
|
|
||||||
}, m.getCacheKeys(data)...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *defaultRedemptionRecordModel) Delete(ctx context.Context, id int64) error {
|
|
||||||
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
|
|
||||||
db := conn
|
|
||||||
return db.Delete(&RedemptionRecord{}, id).Error
|
|
||||||
}, m.getCacheKeys(nil)...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedemptionRecord custom logic methods
|
|
||||||
func (m *customRedemptionRecordModel) QueryRedemptionRecordListByPage(ctx context.Context, page, size int, userId int64, codeId int64) (total int64, list []*RedemptionRecord, err error) {
|
|
||||||
err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
db := conn.Model(&RedemptionRecord{})
|
|
||||||
if userId != 0 {
|
|
||||||
db = db.Where("user_id = ?", userId)
|
|
||||||
}
|
|
||||||
if codeId != 0 {
|
|
||||||
db = db.Where("redemption_code_id = ?", codeId)
|
|
||||||
}
|
|
||||||
return db.Count(&total).Limit(size).Offset((page - 1) * size).Order("created_at DESC").Find(v).Error
|
|
||||||
})
|
|
||||||
return total, list, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *customRedemptionRecordModel) FindByUserId(ctx context.Context, userId int64) ([]*RedemptionRecord, error) {
|
|
||||||
var list []*RedemptionRecord
|
|
||||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
return conn.Model(&RedemptionRecord{}).Where("user_id = ?", userId).Order("created_at DESC").Find(v).Error
|
|
||||||
})
|
|
||||||
return list, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *customRedemptionRecordModel) FindByCodeId(ctx context.Context, codeId int64) ([]*RedemptionRecord, error) {
|
|
||||||
var list []*RedemptionRecord
|
|
||||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
|
||||||
return conn.Model(&RedemptionRecord{}).Where("redemption_code_id = ?", codeId).Order("created_at DESC").Find(v).Error
|
|
||||||
})
|
|
||||||
return list, err
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewRedemptionCodeModel returns a model for the redemption_code table.
|
|
||||||
func NewRedemptionCodeModel(conn *gorm.DB, c *redis.Client) RedemptionCodeModel {
|
|
||||||
return &customRedemptionCodeModel{
|
|
||||||
defaultRedemptionCodeModel: newRedemptionCodeModel(conn, c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRedemptionRecordModel returns a model for the redemption_record table.
|
|
||||||
func NewRedemptionRecordModel(conn *gorm.DB, c *redis.Client) RedemptionRecordModel {
|
|
||||||
return &customRedemptionRecordModel{
|
|
||||||
defaultRedemptionRecordModel: newRedemptionRecordModel(conn, c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
package redemption
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RedemptionCode struct {
|
|
||||||
Id int64 `gorm:"primaryKey"`
|
|
||||||
Code string `gorm:"type:varchar(255);not null;unique;comment:Redemption Code"`
|
|
||||||
TotalCount int64 `gorm:"type:int;not null;default:0;comment:Total Redemption Count"`
|
|
||||||
UsedCount int64 `gorm:"type:int;not null;default:0;comment:Used Redemption Count"`
|
|
||||||
SubscribePlan int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Plan"`
|
|
||||||
UnitTime string `gorm:"type:varchar(50);not null;default:'month';comment:Unit Time: day, month, quarter, half_year, year"`
|
|
||||||
Quantity int64 `gorm:"type:int;not null;default:1;comment:Quantity"`
|
|
||||||
Status int64 `gorm:"type:tinyint;not null;default:1;comment:Status: 1=enabled, 0=disabled"`
|
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index;comment:Delete Time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedemptionRecord struct {
|
|
||||||
Id int64 `gorm:"primaryKey"`
|
|
||||||
RedemptionCodeId int64 `gorm:"type:bigint;not null;default:0;comment:Redemption Code Id;index"`
|
|
||||||
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id;index"`
|
|
||||||
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"`
|
|
||||||
UnitTime string `gorm:"type:varchar(50);not null;default:'month';comment:Unit Time"`
|
|
||||||
Quantity int64 `gorm:"type:int;not null;default:1;comment:Quantity"`
|
|
||||||
RedeemedAt time.Time `gorm:"<-:create;comment:Redeemed Time"`
|
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (RedemptionCode) TableName() string {
|
|
||||||
return "redemption_code"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (RedemptionRecord) TableName() string {
|
|
||||||
return "redemption_record"
|
|
||||||
}
|
|
||||||
@ -61,7 +61,6 @@ type UserFilterParams struct {
|
|||||||
UserId *int64
|
UserId *int64
|
||||||
SubscribeId *int64
|
SubscribeId *int64
|
||||||
UserSubscribeId *int64
|
UserSubscribeId *int64
|
||||||
ShortCode string
|
|
||||||
Order string // Order by id, e.g., "desc"
|
Order string // Order by id, e.g., "desc"
|
||||||
Unscoped bool // Whether to include soft-deleted records
|
Unscoped bool // Whether to include soft-deleted records
|
||||||
}
|
}
|
||||||
@ -147,10 +146,6 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
|
|||||||
conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
|
conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
|
||||||
Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId)
|
Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId)
|
||||||
}
|
}
|
||||||
if filter.ShortCode != "" {
|
|
||||||
conn = conn.Joins("LEFT JOIN user_device ON user.id = user_device.user_id").
|
|
||||||
Where("user_device.short_code LIKE ?", "%"+filter.ShortCode+"%")
|
|
||||||
}
|
|
||||||
if filter.Order != "" {
|
if filter.Order != "" {
|
||||||
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
|
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
|
||||||
}
|
}
|
||||||
@ -158,7 +153,7 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
|
|||||||
conn = conn.Unscoped()
|
conn = conn.Unscoped()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page-1)*size).Preload("UserDevices").Preload("AuthMethods", func(db *gorm.DB) *gorm.DB { return db.Order("user_auth_methods.auth_type desc") }).Find(&list).Error
|
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error
|
||||||
})
|
})
|
||||||
return list, total, err
|
return list, total, err
|
||||||
}
|
}
|
||||||
@ -235,7 +230,7 @@ func (m *customUserModel) QueryResisterUserTotal(ctx context.Context) (int64, er
|
|||||||
func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) {
|
func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) {
|
||||||
var data []*User
|
var data []*User
|
||||||
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
||||||
return conn.Model(&User{}).Preload("AuthMethods", func(db *gorm.DB) *gorm.DB { return db.Order("user_auth_methods.auth_type desc") }).Where("is_admin = ?", true).Find(&data).Error
|
return conn.Model(&User{}).Preload("AuthMethods").Where("is_admin = ?", true).Find(&data).Error
|
||||||
})
|
})
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user