Compare commits
96 Commits
ea94f3c9f9
...
7d46b31866
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d46b31866 | ||
|
|
31e75efacb | ||
|
|
2d4d926924 | ||
|
|
7197f5dcf6 | ||
|
|
9ad602aabe | ||
|
|
bacdf2f301 | ||
|
|
0883fb9370 | ||
|
|
34372fe0b3 | ||
|
|
8022710720 | ||
|
|
b6a1739efa | ||
|
|
7e08a07e29 | ||
|
|
64023dfd1d | ||
|
|
37200698ab | ||
|
|
ffe589ff77 | ||
| 5f1a546bbe | |||
|
|
5f55b1242e | ||
|
|
7d4a19c9a3 | ||
|
|
2a1ae2e1cc | ||
|
|
3359704a45 | ||
|
|
ed669d0620 | ||
|
|
076e5e584b | ||
|
|
d3e18af08e | ||
|
|
69ec491d0a | ||
|
|
d2e9a837cc | ||
|
|
f452838c63 | ||
|
|
3eb40bd5e4 | ||
|
|
8a804eec0c | ||
|
|
8f783b162c | ||
|
|
24c7fc8857 | ||
|
|
23ef9dbff1 | ||
|
|
ec0a0f968e | ||
|
|
3f3b0ae6ad | ||
|
|
518595b058 | ||
|
|
5beff61e91 | ||
|
|
80ee9a6acf | ||
|
|
47c41d1d14 | ||
|
|
39db154e53 | ||
|
|
76ff9a658d | ||
|
|
5e46357104 | ||
|
|
35f60df62e | ||
|
|
b4893b7acc | ||
|
|
adc4a4ff2c | ||
|
|
b8a4b73bb0 | ||
|
|
e062dc1ab0 | ||
|
|
0d57e7eae3 | ||
|
|
5d18c7bb7d | ||
|
|
f0f29deef1 | ||
|
|
22d03a100a | ||
|
|
e81a11cd59 | ||
|
|
128791f43e | ||
|
|
d22659ff04 | ||
|
|
95ddba2332 | ||
|
|
c1efb23354 | ||
|
|
ff2d3f85f3 | ||
|
|
0fb92f380f | ||
|
|
3635d3e224 | ||
|
|
60d584a052 | ||
|
|
b9d3446407 | ||
|
|
902608b2e0 | ||
|
|
d7aa9a44b7 | ||
|
|
b7eafd0892 | ||
|
|
d1a8662095 | ||
|
|
8cce9b95b4 | ||
|
|
066f5d6538 | ||
|
|
de6661cc16 | ||
|
|
52ce054b35 | ||
|
|
2fd119d697 | ||
|
|
2605d22f8e | ||
|
|
829d5f3ffd | ||
|
|
7b8e71ade2 | ||
|
|
aaea4183c2 | ||
|
|
e0003ea074 | ||
|
|
4312e20a5c | ||
|
|
2edc0ef1c8 | ||
|
|
b974b9a56b | ||
|
|
9ab63dff88 | ||
|
|
b2045a6e1b | ||
|
|
cd3b9d4fc8 | ||
|
|
b099331302 | ||
|
|
d78d79fa2d | ||
|
|
fb226a0fc1 | ||
|
|
571628710b | ||
|
|
f67c2e34dc | ||
|
|
15beff410b | ||
|
|
f0e2633ef6 | ||
|
|
b80c7caddd | ||
|
|
37ad4c8443 | ||
|
|
640b8c0805 | ||
|
|
e99058b969 | ||
|
|
39306f3043 | ||
|
|
9ea5c626e9 | ||
|
|
8c776cdbac | ||
|
|
3efa68d3ff | ||
|
|
46e6a9784d | ||
|
|
f3bc933a99 | ||
|
|
71018eb2f4 |
27
.github/environments/production.yml
vendored
Normal file
27
.github/environments/production.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
# 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
Normal file
23
.github/environments/staging.yml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# 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
Normal file
79
.github/workflows/deploy-linux.yml
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
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
51
.github/workflows/develop.yaml
vendored
@ -1,51 +0,0 @@
|
||||
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
131
.github/workflows/release.yml
vendored
@ -1,131 +0,0 @@
|
||||
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
81
.github/workflows/swagger.yaml
vendored
@ -1,81 +0,0 @@
|
||||
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
Normal file
130
.goreleaser.yml
Normal file
@ -0,0 +1,130 @@
|
||||
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.
|
||||
12
.run/go build github.com_perfect-panel_server.run.xml
Normal file
12
.run/go build github.com_perfect-panel_server.run.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<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>
|
||||
95
apis/admin/redemption.api
Normal file
95
apis/admin/redemption.api
Normal file
@ -0,0 +1,95 @@
|
||||
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,6 +22,7 @@ type (
|
||||
Unscoped bool `form:"unscoped,omitempty"`
|
||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||
ShortCode string `form:"short_code,omitempty"`
|
||||
}
|
||||
// GetUserListResponse
|
||||
GetUserListResponse {
|
||||
@ -179,7 +180,7 @@ type (
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
DeleteUserSubscribeRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
UserSubscribeId int64 `json:"user_subscribe_id,string"`
|
||||
}
|
||||
GetUserSubscribeByIdRequest {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
|
||||
@ -124,6 +124,7 @@ type (
|
||||
IP string `header:"X-Original-Forwarded-For"`
|
||||
UserAgent string `json:"user_agent" validate:"required"`
|
||||
CfToken string `json:"cf_token,optional"`
|
||||
ShortCode string `json:"short_code,optional"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
32
apis/public/redemption.api
Normal file
32
apis/public/redemption.api
Normal file
@ -0,0 +1,32 @@
|
||||
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,40 +14,48 @@ type (
|
||||
QuerySubscribeListRequest {
|
||||
Language string `form:"language"`
|
||||
}
|
||||
QueryUserSubscribeNodeListResponse {
|
||||
List []UserSubscribeInfo `json:"list"`
|
||||
}
|
||||
UserSubscribeInfo {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
IsTryOut bool `json:"is_try_out"`
|
||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||
}
|
||||
UserSubscribeNodeInfo {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Uuid string `json:"uuid"`
|
||||
Protocol string `json:"protocol"`
|
||||
Port uint16 `json:"port"`
|
||||
Address string `json:"address"`
|
||||
Tags []string `json:"tags"`
|
||||
Country string `json:"country"`
|
||||
City string `json:"city"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
QueryUserSubscribeNodeListResponse {
|
||||
List []UserSubscribeInfo `json:"list"`
|
||||
}
|
||||
|
||||
UserSubscribeInfo {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
OrderId int64 `json:"order_id"`
|
||||
SubscribeId int64 `json:"subscribe_id"`
|
||||
StartTime int64 `json:"start_time"`
|
||||
ExpireTime int64 `json:"expire_time"`
|
||||
FinishedAt int64 `json:"finished_at"`
|
||||
ResetTime int64 `json:"reset_time"`
|
||||
Traffic int64 `json:"traffic"`
|
||||
Download int64 `json:"download"`
|
||||
Upload int64 `json:"upload"`
|
||||
Token string `json:"token"`
|
||||
Status uint8 `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
IsTryOut bool `json:"is_try_out"`
|
||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||
}
|
||||
|
||||
UserSubscribeNodeInfo{
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Uuid string `json:"uuid"`
|
||||
Protocol string `json:"protocol"`
|
||||
Protocols string `json:"protocols"`
|
||||
Port uint16 `json:"port"`
|
||||
Address string `json:"address"`
|
||||
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 (
|
||||
@ -60,8 +68,8 @@ service ppanel {
|
||||
@handler QuerySubscribeList
|
||||
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
|
||||
|
||||
@doc "Get user subscribe node info"
|
||||
@handler QueryUserSubscribeNodeList
|
||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||
@doc "Get user subscribe node info"
|
||||
@handler QueryUserSubscribeNodeList
|
||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||
}
|
||||
|
||||
|
||||
@ -66,6 +66,7 @@ type (
|
||||
UnbindOAuthRequest {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
|
||||
GetLoginLogRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@ -94,17 +95,21 @@ type (
|
||||
Email string `json:"email" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
GetDeviceListResponse {
|
||||
List []UserDevice `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
UnbindDeviceRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
GetDeviceListResponse {
|
||||
List []UserDevice `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
UnbindDeviceRequest {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
UpdateUserSubscribeNoteRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||
Note string `json:"note" validate:"max=500"`
|
||||
}
|
||||
|
||||
UpdateUserRulesRequest {
|
||||
Rules []string `json:"rules" validate:"required"`
|
||||
}
|
||||
@ -130,6 +135,23 @@ type (
|
||||
List []WithdrawalLog `json:"list"`
|
||||
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 (
|
||||
@ -226,9 +248,9 @@ service ppanel {
|
||||
@handler UpdateBindEmail
|
||||
put /bind_email (UpdateBindEmailRequest)
|
||||
|
||||
@doc "Get Device List"
|
||||
@handler GetDeviceList
|
||||
get /devices returns (GetDeviceListResponse)
|
||||
@doc "Get Device List"
|
||||
@handler GetDeviceList
|
||||
get /devices returns (GetDeviceListResponse)
|
||||
|
||||
@doc "Unbind Device"
|
||||
@handler UnbindDevice
|
||||
@ -249,5 +271,24 @@ service ppanel {
|
||||
@doc "Query Withdrawal Log"
|
||||
@handler QueryWithdrawalLog
|
||||
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,6 +32,7 @@ type (
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||
IsDel bool `json:"is_del,omitempty"`
|
||||
}
|
||||
Follow {
|
||||
Id int64 `json:"id"`
|
||||
@ -150,6 +151,7 @@ type (
|
||||
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
||||
IpRegisterLimit int64 `json:"ip_register_limit"`
|
||||
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
||||
DeviceLimit int64 `json:"device_limit"`
|
||||
}
|
||||
VerifyConfig {
|
||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||
@ -204,7 +206,7 @@ type (
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
}
|
||||
SubscribeDiscount {
|
||||
Quantity int64 `json:"quantity"`
|
||||
Quantity int64 `json:"quantity"`
|
||||
Discount float64 `json:"discount"`
|
||||
}
|
||||
Subscribe {
|
||||
@ -446,6 +448,28 @@ type (
|
||||
CreatedAt int64 `json:"created_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 {
|
||||
Id int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@ -656,7 +680,7 @@ type (
|
||||
// public announcement
|
||||
QueryAnnouncementRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Size int `form:"size,default=15"`
|
||||
Pinned *bool `form:"pinned"`
|
||||
Popup *bool `form:"popup"`
|
||||
}
|
||||
@ -673,6 +697,7 @@ type (
|
||||
List []SubscribeGroup `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
GetUserSubscribeTrafficLogsRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
|
||||
BIN
cache/GeoLite2-City.mmdb
vendored
Normal file
BIN
cache/GeoLite2-City.mmdb
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 MiB |
73
cmd/update.go
Normal file
73
cmd/update.go
Normal file
@ -0,0 +1,73 @@
|
||||
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.")
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
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/klauspost/compress v1.17.7
|
||||
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/smartwalle/alipay/v3 v3.2.23
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
|
||||
@ -1 +1,2 @@
|
||||
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);
|
||||
CREATE INDEX idx_timestamp ON traffic_log (timestamp);
|
||||
|
||||
|
||||
0
initialize/migrate/database/02119_node.down.sql
Normal file
0
initialize/migrate/database/02119_node.down.sql
Normal file
78
initialize/migrate/database/02119_node.up.sql
Normal file
78
initialize/migrate/database/02119_node.up.sql
Normal file
@ -0,0 +1,78 @@
|
||||
-- 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;
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
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 +0,0 @@
|
||||
DROP TABLE IF EXISTS `server`;
|
||||
5
initialize/migrate/database/02126_redemption.down.sql
Normal file
5
initialize/migrate/database/02126_redemption.down.sql
Normal file
@ -0,0 +1,5 @@
|
||||
-- Drop redemption_record table
|
||||
DROP TABLE IF EXISTS `redemption_record`;
|
||||
|
||||
-- Drop redemption_code table
|
||||
DROP TABLE IF EXISTS `redemption_code`;
|
||||
31
initialize/migrate/database/02126_redemption.up.sql
Normal file
31
initialize/migrate/database/02126_redemption.up.sql
Normal file
@ -0,0 +1,31 @@
|
||||
-- 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';
|
||||
@ -0,0 +1,2 @@
|
||||
-- Remove status column from redemption_code table
|
||||
ALTER TABLE `redemption_code` DROP COLUMN `status`;
|
||||
@ -0,0 +1,2 @@
|
||||
-- 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`;
|
||||
2
initialize/migrate/database/02128_device_limit.down.sql
Normal file
2
initialize/migrate/database/02128_device_limit.down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
-- Remove device limit configuration from system table
|
||||
DELETE FROM `system` WHERE `category` = 'register' AND `key` = 'DeviceLimit';
|
||||
3
initialize/migrate/database/02128_device_limit.up.sql
Normal file
3
initialize/migrate/database/02128_device_limit.up.sql
Normal file
@ -0,0 +1,3 @@
|
||||
-- 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());
|
||||
@ -0,0 +1,2 @@
|
||||
-- Remove short_code column from user_device table
|
||||
ALTER TABLE `user_device` DROP COLUMN `short_code`;
|
||||
@ -0,0 +1,2 @@
|
||||
-- Add short_code column to user_device table
|
||||
ALTER TABLE `user_device` ADD COLUMN `short_code` VARCHAR(255) DEFAULT '' COMMENT 'Short Code' AFTER `identifier`;
|
||||
@ -0,0 +1,2 @@
|
||||
-- Remove index on refer_code column
|
||||
ALTER TABLE `user` DROP INDEX `idx_refer_code`;
|
||||
@ -0,0 +1,2 @@
|
||||
-- Add index on refer_code column for faster lookup
|
||||
ALTER TABLE `user` ADD INDEX `idx_refer_code` (`refer_code`);
|
||||
@ -39,6 +39,9 @@ const VerifyCodeConfigKey = "system:verify_code_config"
|
||||
// SessionIdKey cache session key
|
||||
const SessionIdKey = "auth:session_id"
|
||||
|
||||
// DeviceCacheKeyKey cache session key
|
||||
const DeviceCacheKeyKey = "auth:device_identifier"
|
||||
|
||||
// GlobalConfigKey Global Config Key
|
||||
const GlobalConfigKey = "system:global_config"
|
||||
|
||||
@ -59,3 +62,5 @@ const SendIntervalKeyPrefix = "send:interval:"
|
||||
|
||||
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
|
||||
const SendCountLimitKeyPrefix = "send:limit:"
|
||||
|
||||
const RegisterIpKeyPrefix = "register:ip:"
|
||||
|
||||
@ -37,9 +37,18 @@ type Config struct {
|
||||
}
|
||||
|
||||
type RedisConfig struct {
|
||||
Host string `yaml:"Host" default:"localhost:6379"`
|
||||
Pass string `yaml:"Pass" default:""`
|
||||
DB int `yaml:"DB" default:"0"`
|
||||
Host string `yaml:"Host" default:"localhost:6379"`
|
||||
Pass string `yaml:"Pass" default:""`
|
||||
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 {
|
||||
@ -73,6 +82,7 @@ type RegisterConfig struct {
|
||||
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
||||
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
||||
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
||||
DeviceLimit int64 `yaml:"DeviceLimit" default:"5"`
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
26
internal/handler/public/redemption/redeemCodeHandler.go
Normal file
26
internal/handler/public/redemption/redeemCodeHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
29
internal/handler/public/user/ws/deviceWsConnectHandler.go
Normal file
29
internal/handler/public/user/ws/deviceWsConnectHandler.go
Normal file
@ -0,0 +1,29 @@
|
||||
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,6 +16,7 @@ import (
|
||||
adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing"
|
||||
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
|
||||
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"
|
||||
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
|
||||
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system"
|
||||
@ -30,9 +31,11 @@ import (
|
||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||
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"
|
||||
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
|
||||
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"
|
||||
"github.com/perfect-panel/server/internal/middleware"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@ -298,6 +301,32 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
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.Use(middleware.AuthMiddleware(serverCtx))
|
||||
|
||||
@ -748,6 +777,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
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.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||
|
||||
@ -813,6 +850,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
// Commission Withdraw
|
||||
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
|
||||
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
|
||||
|
||||
@ -868,6 +911,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
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.Use(middleware.ServerMiddleware(serverCtx))
|
||||
|
||||
@ -888,10 +939,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||
}
|
||||
|
||||
serverGroupRouter := router.Group("/v2/server")
|
||||
serverV2GroupRouter := router.Group("/v2/server")
|
||||
|
||||
{
|
||||
// Get Server Protocol Config
|
||||
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
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
|
||||
}
|
||||
120
internal/logic/admin/redemption/createRedemptionCodeLogic.go
Normal file
120
internal/logic/admin/redemption/createRedemptionCodeLogic.go
Normal file
@ -0,0 +1,120 @@
|
||||
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
|
||||
}
|
||||
36
internal/logic/admin/redemption/deleteRedemptionCodeLogic.go
Normal file
36
internal/logic/admin/redemption/deleteRedemptionCodeLogic.go
Normal file
@ -0,0 +1,36 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
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
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
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
|
||||
}
|
||||
65
internal/logic/admin/redemption/updateRedemptionCodeLogic.go
Normal file
65
internal/logic/admin/redemption/updateRedemptionCodeLogic.go
Normal file
@ -0,0 +1,65 @@
|
||||
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,6 +99,10 @@ func (l *CreateServerLogic) CreateServer(req *types.CreateServerRequest) error {
|
||||
} else {
|
||||
data.City = result.City
|
||||
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)
|
||||
|
||||
@ -39,7 +39,7 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
|
||||
data.Country = req.Country
|
||||
data.City = req.City
|
||||
// only update address when it's different
|
||||
if req.Address != data.Address {
|
||||
if req.Address != data.Address || (data.Country == "" || req.Country == "") {
|
||||
// query server ip location
|
||||
result, err := ip.GetRegionByIp(req.Address)
|
||||
if err != nil {
|
||||
@ -47,6 +47,10 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
|
||||
} else {
|
||||
data.City = result.City
|
||||
data.Country = result.Country
|
||||
data.Latitude = result.Latitude
|
||||
data.Longitude = result.Longitude
|
||||
data.LatitudeCenter = result.LatitudeCenter
|
||||
data.LongitudeCenter = result.LongitudeCenter
|
||||
}
|
||||
// update address
|
||||
data.Address = req.Address
|
||||
|
||||
@ -2,14 +2,12 @@ package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetModuleConfigLogic struct {
|
||||
@ -28,14 +26,14 @@ func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *G
|
||||
}
|
||||
|
||||
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
|
||||
value, exists := os.LookupEnv("SECRET_KEY")
|
||||
if !exists {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
||||
}
|
||||
//value, exists := os.LookupEnv("SECRET_KEY")
|
||||
//if !exists {
|
||||
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
||||
//}
|
||||
|
||||
return &types.ModuleConfig{
|
||||
Secret: value,
|
||||
//Secret: value,
|
||||
ServiceName: constant.ServiceName,
|
||||
ServiceVersion: constant.Version,
|
||||
ServiceVersion: strings.ReplaceAll(constant.Version, "v", ""),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -48,5 +48,9 @@ func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubs
|
||||
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())
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
Unscoped: req.Unscoped,
|
||||
SubscribeId: req.SubscribeId,
|
||||
UserSubscribeId: req.UserSubscribeId,
|
||||
ShortCode: req.ShortCode,
|
||||
Order: "DESC",
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@ -69,5 +69,10 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs
|
||||
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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@ -88,6 +89,36 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
||||
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 {
|
||||
// Create device auth method
|
||||
authMethod := &user.AuthMethods{
|
||||
@ -107,8 +138,9 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
||||
|
||||
// Create device record
|
||||
deviceInfo := &user.Device{
|
||||
Ip: ip,
|
||||
UserId: userId,
|
||||
Ip: ip,
|
||||
UserId: userId,
|
||||
|
||||
UserAgent: userAgent,
|
||||
Identifier: identifier,
|
||||
Enabled: true,
|
||||
@ -146,10 +178,87 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
||||
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
|
||||
oldUserId := deviceInfo.UserId
|
||||
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Check if old user has other auth methods besides device
|
||||
var authMethods []user.AuthMethods
|
||||
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
|
||||
// Check device limit for new user
|
||||
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
|
||||
if deviceLimit > 0 {
|
||||
// Count new user's current devices (excluding the one being rebound)
|
||||
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",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("error", err.Error()),
|
||||
@ -157,60 +266,113 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
|
||||
}
|
||||
|
||||
// Count non-device auth methods
|
||||
nonDeviceAuthCount := 0
|
||||
for _, auth := range authMethods {
|
||||
if auth.AuthType != "device" {
|
||||
nonDeviceAuthCount++
|
||||
//如果没有其他认证方式,禁用旧用户账号
|
||||
if count < 1 {
|
||||
//检查设备下是否有套餐,有套餐。就检查即将绑定过去的所有账户是否有套餐,如果有,那么检查两个套餐是否一致。如果一致就将即将删除的用户套餐,时间叠加到我绑定过去的用户套餐上面(如果套餐已过期就忽略)。新绑定设备的账户上套餐不一致或者不存在直接将套餐换绑即可
|
||||
var oldUserSubscribes []user.Subscribe
|
||||
err = tx.Where("user_id = ? AND status IN ?", oldUserId, []int64{0, 1}).Find(&oldUserSubscribes).Error
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Only disable old user if they have no other auth methods
|
||||
if nonDeviceAuthCount == 0 {
|
||||
falseVal := false
|
||||
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
|
||||
if len(oldUserSubscribes) > 0 {
|
||||
l.Infow("processing old user subscribes",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("new_user_id", newUserId),
|
||||
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",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
// Update device auth method to new user
|
||||
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)
|
||||
}
|
||||
l.Infow("disabled old user (no other auth methods)",
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
)
|
||||
|
||||
// Update device record
|
||||
// 更新设备绑定的用户id
|
||||
deviceInfo.UserId = newUserId
|
||||
deviceInfo.Ip = ip
|
||||
deviceInfo.UserAgent = userAgent
|
||||
deviceInfo.Enabled = true
|
||||
|
||||
if err := db.Save(deviceInfo).Error; err != nil {
|
||||
if err := tx.Save(deviceInfo).Error; err != nil {
|
||||
l.Errorw("failed to update device",
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@ -224,6 +386,15 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
|
||||
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",
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("old_user_id", oldUserId),
|
||||
|
||||
@ -71,6 +71,9 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
||||
if err != nil {
|
||||
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
|
||||
userInfo, err = l.registerUserAndDevice(req)
|
||||
if err != nil {
|
||||
@ -125,6 +128,17 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
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
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
@ -138,9 +152,11 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
||||
)
|
||||
|
||||
var userInfo *user.User
|
||||
var trialSubscribe *user.Subscribe
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Create new user
|
||||
userInfo = &user.User{
|
||||
Salt: "default",
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
}
|
||||
if err := db.Create(userInfo).Error; err != nil {
|
||||
@ -182,6 +198,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
||||
UserId: userInfo.Id,
|
||||
UserAgent: req.UserAgent,
|
||||
Identifier: req.Identifier,
|
||||
ShortCode: req.ShortCode,
|
||||
Enabled: true,
|
||||
Online: false,
|
||||
}
|
||||
@ -196,8 +213,10 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
||||
|
||||
// Activate trial if enabled
|
||||
if l.svcCtx.Config.Register.EnableTrial {
|
||||
if err := l.activeTrial(userInfo.Id, db); err != nil {
|
||||
return err
|
||||
var trialErr error
|
||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, db)
|
||||
if trialErr != nil {
|
||||
return trialErr
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,6 +231,25 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
||||
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",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
@ -244,7 +282,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
||||
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscribe, error) {
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||
if err != nil {
|
||||
l.Errorw("failed to find trial subscription template",
|
||||
@ -252,7 +290,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
||||
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
@ -279,7 +317,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Infow("trial subscription activated successfully",
|
||||
@ -289,5 +327,5 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
||||
logger.Field("traffic", sub.Traffic),
|
||||
)
|
||||
|
||||
return nil
|
||||
return userSub, nil
|
||||
}
|
||||
|
||||
@ -341,6 +341,7 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
||||
}
|
||||
|
||||
var userInfo *user.User
|
||||
var trialSubscribe *user.Subscribe
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
if email != "" {
|
||||
l.Debugw("checking if email already exists",
|
||||
@ -397,8 +398,10 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
||||
logger.Field("request_id", requestID),
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
)
|
||||
if err := l.activeTrial(userInfo.Id, requestID); err != nil {
|
||||
return err
|
||||
var trialErr error
|
||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, requestID)
|
||||
if trialErr != nil {
|
||||
return trialErr
|
||||
}
|
||||
}
|
||||
|
||||
@ -415,6 +418,25 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
||||
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",
|
||||
logger.Field("request_id", requestID),
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
@ -793,7 +815,7 @@ func (l *OAuthLoginGetTokenLogic) findOrRegisterUser(authType, openID, email, av
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error {
|
||||
func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*user.Subscribe, error) {
|
||||
l.Debugw("fetching trial subscription template",
|
||||
logger.Field("request_id", requestID),
|
||||
logger.Field("user_id", uid),
|
||||
@ -808,7 +830,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error
|
||||
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
@ -848,7 +870,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error
|
||||
logger.Field("user_id", uid),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Infow("trial subscription activated successfully",
|
||||
@ -858,5 +880,5 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error
|
||||
logger.Field("expire_time", expireTime),
|
||||
logger.Field("traffic", sub.Traffic),
|
||||
)
|
||||
return nil
|
||||
return userSub, nil
|
||||
}
|
||||
|
||||
65
internal/logic/auth/registerLimitLogic.go
Normal file
65
internal/logic/auth/registerLimitLogic.go
Normal file
@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
@ -133,7 +133,8 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
jwt.WithOption("identifier", req.Identifier),
|
||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@ -10,7 +10,6 @@ import (
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/logic/common"
|
||||
"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/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
@ -48,7 +47,22 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
||||
}
|
||||
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
|
||||
defer func(svcCtx *svc.ServiceContext) {
|
||||
if userInfo.Id != 0 {
|
||||
@ -76,22 +90,6 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
}
|
||||
}(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 == "" {
|
||||
return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty")
|
||||
}
|
||||
@ -137,8 +135,8 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
}
|
||||
}
|
||||
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||
}
|
||||
|
||||
// Generate session id
|
||||
@ -150,7 +148,8 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
jwt.WithOption("identifier", req.Identifier),
|
||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@ -43,19 +44,32 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
||||
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")
|
||||
}
|
||||
|
||||
// if the email verification is enabled, the verification code is required
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber)
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(uint8(constant.Security)), phoneNumber)
|
||||
l.Logger.Infof("TelephoneResetPassword cacheKey: %s, code: %s", cacheKey, code)
|
||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||
if err != nil {
|
||||
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
||||
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")
|
||||
}
|
||||
|
||||
if value != code {
|
||||
var payload CacheKeyPayload
|
||||
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")
|
||||
}
|
||||
|
||||
@ -96,8 +110,8 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
@ -108,7 +122,8 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
jwt.WithOption("identifier", req.Identifier),
|
||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@ -45,6 +45,7 @@ func NewTelephoneUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceConte
|
||||
|
||||
func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
|
||||
c := l.svcCtx.Config.Register
|
||||
var trialSubscribe *user.Subscribe
|
||||
// Check if the registration is stopped
|
||||
if c.StopRegister {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
||||
@ -102,7 +103,9 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
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
|
||||
pwd := tool.EncodePassWord(req.Password)
|
||||
userInfo := &user.User{
|
||||
@ -133,12 +136,36 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
}
|
||||
if l.svcCtx.Config.Register.EnableTrial {
|
||||
// Active trial
|
||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||
return err
|
||||
var trialErr error
|
||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
|
||||
if trialErr != nil {
|
||||
return trialErr
|
||||
}
|
||||
}
|
||||
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
|
||||
if req.Identifier != "" {
|
||||
@ -152,8 +179,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
@ -164,7 +191,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
jwt.WithOption("identifier", req.Identifier),
|
||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
@ -226,10 +254,10 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) error {
|
||||
func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
userSub := &user.Subscribe{
|
||||
Id: 0,
|
||||
@ -245,5 +273,10 @@ func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) error {
|
||||
UUID: uuidx.NewUUID().String(),
|
||||
Status: 1,
|
||||
}
|
||||
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
|
||||
err = 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
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
@ -109,7 +109,8 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", req.LoginType),
|
||||
jwt.WithOption("identifier", req.Identifier),
|
||||
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
|
||||
@ -42,6 +42,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
c := l.svcCtx.Config.Register
|
||||
email := l.svcCtx.Config.Email
|
||||
var referer *user.User
|
||||
var trialSubscribe *user.Subscribe
|
||||
// Check if the registration is stopped
|
||||
if c.StopRegister {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
||||
@ -89,6 +90,10 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
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
|
||||
pwd := tool.EncodePassWord(req.Password)
|
||||
userInfo := &user.User{
|
||||
@ -123,12 +128,36 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
|
||||
if l.svcCtx.Config.Register.EnableTrial {
|
||||
// Active trial
|
||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||
return err
|
||||
var trialErr error
|
||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
|
||||
if trialErr != nil {
|
||||
return trialErr
|
||||
}
|
||||
}
|
||||
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
|
||||
if req.Identifier != "" {
|
||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
@ -141,8 +170,8 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
// Don't fail register if device binding fails, just log the error
|
||||
}
|
||||
}
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
@ -216,10 +245,10 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *UserRegisterLogic) activeTrial(uid int64) error {
|
||||
func (l *UserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
|
||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
userSub := &user.Subscribe{
|
||||
UserId: uid,
|
||||
@ -234,5 +263,8 @@ func (l *UserRegisterLogic) activeTrial(uid int64) error {
|
||||
UUID: uuidx.NewUUID().String(),
|
||||
Status: 1,
|
||||
}
|
||||
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
|
||||
if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil {
|
||||
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()))
|
||||
return err
|
||||
}
|
||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
|
||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
||||
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||
if err != nil {
|
||||
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()))
|
||||
return err
|
||||
}
|
||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
||||
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||
if err != nil {
|
||||
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))
|
||||
return err
|
||||
}
|
||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
|
||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
||||
_, err = l.svcCtx.Queue.Enqueue(task)
|
||||
if err != nil {
|
||||
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
||||
|
||||
@ -55,6 +55,25 @@ 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))
|
||||
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
|
||||
if sub.Discount != "" {
|
||||
var dis []types.SubscribeDiscount
|
||||
@ -98,18 +117,6 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
||||
couponAmount = calculateCoupon(amount, couponInfo)
|
||||
}
|
||||
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
|
||||
if req.Payment != 0 {
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
@ -120,8 +127,19 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
||||
// Calculate the handling fee
|
||||
if amount > 0 {
|
||||
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{
|
||||
|
||||
@ -93,19 +93,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
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
|
||||
if sub.Discount != "" {
|
||||
var dis []types.SubscribeDiscount
|
||||
@ -160,19 +147,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
}
|
||||
// Calculate the handling fee
|
||||
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
|
||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||
if err != nil {
|
||||
@ -194,6 +168,17 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
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
|
||||
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
@ -221,9 +206,28 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
}
|
||||
// Database transaction
|
||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
// check subscribe plan quota limit inside transaction to prevent race condition
|
||||
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 {
|
||||
// update user deduction && Pre deduction ,Return after canceling the order
|
||||
// deduct gift amount from user
|
||||
u.GiftAmount -= orderInfo.GiftAmount
|
||||
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))
|
||||
return e
|
||||
|
||||
@ -366,6 +366,7 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
|
||||
}
|
||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||
}
|
||||
|
||||
// Create payment URL for user redirection
|
||||
url := client.CreatePayUrl(epay.Order{
|
||||
Name: l.svcCtx.Config.Site.SiteName,
|
||||
|
||||
@ -150,7 +150,7 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
|
||||
}
|
||||
content, _ := tempOrder.Marshal()
|
||||
|
||||
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), CloseOrderTimeMinutes*time.Minute).Result(); err != nil {
|
||||
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), 24*time.Hour).Result(); err != nil {
|
||||
l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo))
|
||||
return err
|
||||
}
|
||||
|
||||
224
internal/logic/public/redemption/redeemCodeLogic.go
Normal file
224
internal/logic/public/redemption/redeemCodeLogic.go
Normal file
@ -0,0 +1,224 @@
|
||||
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")
|
||||
}
|
||||
|
||||
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 2)
|
||||
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 0, 1, 2, 3)
|
||||
if err != nil {
|
||||
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")
|
||||
@ -79,7 +79,6 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
|
||||
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
|
||||
userSubscribeInfo.IsTryOut = true
|
||||
}
|
||||
|
||||
resp.List = append(resp.List, userSubscribeInfo)
|
||||
}
|
||||
|
||||
@ -137,16 +136,21 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
|
||||
continue
|
||||
}
|
||||
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
||||
Id: n.Id,
|
||||
Name: n.Name,
|
||||
Uuid: userSub.UUID,
|
||||
Protocol: n.Protocol,
|
||||
Port: n.Port,
|
||||
Address: n.Address,
|
||||
Tags: strings.Split(n.Tags, ","),
|
||||
Country: server.Country,
|
||||
City: server.City,
|
||||
CreatedAt: n.CreatedAt.Unix(),
|
||||
Id: n.Id,
|
||||
Name: n.Name,
|
||||
Uuid: userSub.UUID,
|
||||
Protocol: n.Protocol,
|
||||
Protocols: server.Protocols,
|
||||
Port: n.Port,
|
||||
Address: n.Address,
|
||||
Tags: strings.Split(n.Tags, ","),
|
||||
Country: server.Country,
|
||||
City: server.City,
|
||||
Latitude: server.Latitude,
|
||||
Longitude: server.Longitude,
|
||||
LongitudeCenter: server.LongitudeCenter,
|
||||
LatitudeCenter: server.LatitudeCenter,
|
||||
CreatedAt: n.CreatedAt.Unix(),
|
||||
}
|
||||
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
|
||||
}
|
||||
|
||||
86
internal/logic/public/user/deleteCurrentUserAccountLogic.go
Normal file
86
internal/logic/public/user/deleteCurrentUserAccountLogic.go
Normal file
@ -0,0 +1,86 @@
|
||||
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
|
||||
|
||||
}
|
||||
115
internal/logic/public/user/deviceOnlineStatisticsLogic.go
Normal file
115
internal/logic/public/user/deviceOnlineStatisticsLogic.go
Normal file
@ -0,0 +1,115 @@
|
||||
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,6 +83,9 @@ func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetU
|
||||
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())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -64,9 +64,23 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
|
||||
}
|
||||
sessionId := l.ctx.Value(constant.CtxKeySessionID)
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey)
|
||||
var count int64
|
||||
err = tx.Model(user.AuthMethods{}).Where("user_id = ?", deleteDevice.UserId).Count(&count).Error
|
||||
if err != nil {
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
87
internal/logic/public/user/ws/deviceWsConnectLogic.go
Normal file
87
internal/logic/public/user/ws/deviceWsConnectLogic.go
Normal file
@ -0,0 +1,87 @@
|
||||
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,8 +42,11 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
||||
}
|
||||
|
||||
loginType := ""
|
||||
if claims["LoginType"] != nil {
|
||||
loginType = claims["LoginType"].(string)
|
||||
if claims["CtxLoginType"] != nil {
|
||||
loginType = claims["CtxLoginType"].(string)
|
||||
}
|
||||
if claims["identifier"] != nil {
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyIdentifier, claims["identifier"].(string))
|
||||
}
|
||||
// get user id from token
|
||||
userId := int64(claims["UserId"].(float64))
|
||||
@ -82,9 +85,10 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
ctx = context.WithValue(ctx, constant.LoginType, loginType)
|
||||
ctx = context.WithValue(ctx, constant.CtxLoginType, loginType)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
||||
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@ -42,11 +42,11 @@ func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
|
||||
ctx := c.Request.Context()
|
||||
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
|
||||
ctx = context.WithValue(ctx, constant.LoginType, c.GetHeader("Login-Type"))
|
||||
ctx = context.WithValue(ctx, constant.CtxLoginType, c.GetHeader("Login-Type"))
|
||||
c.Request = c.Request.WithContext(ctx)
|
||||
}
|
||||
|
||||
loginType, ok := ctx.Value(constant.LoginType).(string)
|
||||
loginType, ok := ctx.Value(constant.CtxLoginType).(string)
|
||||
if !ok || loginType != "device" {
|
||||
c.Next()
|
||||
return
|
||||
|
||||
@ -27,6 +27,9 @@ type Filter struct {
|
||||
|
||||
// GetAnnouncementListByPage get announcement list by page
|
||||
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
|
||||
if size == 0 {
|
||||
size = 10
|
||||
}
|
||||
var list []*Announcement
|
||||
var total int64
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
|
||||
@ -13,6 +13,7 @@ type customServerLogicModel interface {
|
||||
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
|
||||
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
|
||||
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
|
||||
ClearServerAllCache(ctx context.Context) error
|
||||
}
|
||||
|
||||
const (
|
||||
@ -171,6 +172,30 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
|
||||
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 查询
|
||||
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
|
||||
@ -15,12 +15,16 @@ type Server struct {
|
||||
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
|
||||
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"`
|
||||
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
|
||||
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
|
||||
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
|
||||
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
|
||||
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
|
||||
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
|
||||
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
|
||||
Longitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Longitude"`
|
||||
Latitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Latitude"`
|
||||
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 {
|
||||
|
||||
288
internal/model/redemption/default.go
Normal file
288
internal/model/redemption/default.go
Normal file
@ -0,0 +1,288 @@
|
||||
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
|
||||
}
|
||||
20
internal/model/redemption/model.go
Normal file
20
internal/model/redemption/model.go
Normal file
@ -0,0 +1,20 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
40
internal/model/redemption/redemption.go
Normal file
40
internal/model/redemption/redemption.go
Normal file
@ -0,0 +1,40 @@
|
||||
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,6 +61,7 @@ type UserFilterParams struct {
|
||||
UserId *int64
|
||||
SubscribeId *int64
|
||||
UserSubscribeId *int64
|
||||
ShortCode string
|
||||
Order string // Order by id, e.g., "desc"
|
||||
Unscoped bool // Whether to include soft-deleted records
|
||||
}
|
||||
@ -146,6 +147,10 @@ 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").
|
||||
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 != "" {
|
||||
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
|
||||
}
|
||||
@ -153,7 +158,7 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
|
||||
conn = conn.Unscoped()
|
||||
}
|
||||
}
|
||||
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error
|
||||
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 list, total, err
|
||||
}
|
||||
@ -230,7 +235,7 @@ func (m *customUserModel) QueryResisterUserTotal(ctx context.Context) (int64, er
|
||||
func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) {
|
||||
var data []*User
|
||||
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Model(&User{}).Preload("AuthMethods").Where("is_admin = ?", true).Find(&data).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 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