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"`
|
Unscoped bool `form:"unscoped,omitempty"`
|
||||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||||
|
ShortCode string `form:"short_code,omitempty"`
|
||||||
}
|
}
|
||||||
// GetUserListResponse
|
// GetUserListResponse
|
||||||
GetUserListResponse {
|
GetUserListResponse {
|
||||||
@ -179,7 +180,7 @@ type (
|
|||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
DeleteUserSubscribeRequest {
|
DeleteUserSubscribeRequest {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
UserSubscribeId int64 `json:"user_subscribe_id,string"`
|
||||||
}
|
}
|
||||||
GetUserSubscribeByIdRequest {
|
GetUserSubscribeByIdRequest {
|
||||||
Id int64 `form:"id" validate:"required"`
|
Id int64 `form:"id" validate:"required"`
|
||||||
|
|||||||
@ -124,6 +124,7 @@ type (
|
|||||||
IP string `header:"X-Original-Forwarded-For"`
|
IP string `header:"X-Original-Forwarded-For"`
|
||||||
UserAgent string `json:"user_agent" validate:"required"`
|
UserAgent string `json:"user_agent" validate:"required"`
|
||||||
CfToken string `json:"cf_token,optional"`
|
CfToken string `json:"cf_token,optional"`
|
||||||
|
ShortCode string `json:"short_code,optional"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
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 {
|
QuerySubscribeListRequest {
|
||||||
Language string `form:"language"`
|
Language string `form:"language"`
|
||||||
}
|
}
|
||||||
QueryUserSubscribeNodeListResponse {
|
|
||||||
List []UserSubscribeInfo `json:"list"`
|
QueryUserSubscribeNodeListResponse {
|
||||||
}
|
List []UserSubscribeInfo `json:"list"`
|
||||||
UserSubscribeInfo {
|
}
|
||||||
Id int64 `json:"id"`
|
|
||||||
UserId int64 `json:"user_id"`
|
UserSubscribeInfo {
|
||||||
OrderId int64 `json:"order_id"`
|
Id int64 `json:"id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
UserId int64 `json:"user_id"`
|
||||||
StartTime int64 `json:"start_time"`
|
OrderId int64 `json:"order_id"`
|
||||||
ExpireTime int64 `json:"expire_time"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
FinishedAt int64 `json:"finished_at"`
|
StartTime int64 `json:"start_time"`
|
||||||
ResetTime int64 `json:"reset_time"`
|
ExpireTime int64 `json:"expire_time"`
|
||||||
Traffic int64 `json:"traffic"`
|
FinishedAt int64 `json:"finished_at"`
|
||||||
Download int64 `json:"download"`
|
ResetTime int64 `json:"reset_time"`
|
||||||
Upload int64 `json:"upload"`
|
Traffic int64 `json:"traffic"`
|
||||||
Token string `json:"token"`
|
Download int64 `json:"download"`
|
||||||
Status uint8 `json:"status"`
|
Upload int64 `json:"upload"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
Token string `json:"token"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
Status uint8 `json:"status"`
|
||||||
IsTryOut bool `json:"is_try_out"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
IsTryOut bool `json:"is_try_out"`
|
||||||
UserSubscribeNodeInfo {
|
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
|
||||||
Id int64 `json:"id"`
|
}
|
||||||
Name string `json:"name"`
|
|
||||||
Uuid string `json:"uuid"`
|
UserSubscribeNodeInfo{
|
||||||
Protocol string `json:"protocol"`
|
Id int64 `json:"id"`
|
||||||
Port uint16 `json:"port"`
|
Name string `json:"name"`
|
||||||
Address string `json:"address"`
|
Uuid string `json:"uuid"`
|
||||||
Tags []string `json:"tags"`
|
Protocol string `json:"protocol"`
|
||||||
Country string `json:"country"`
|
Protocols string `json:"protocols"`
|
||||||
City string `json:"city"`
|
Port uint16 `json:"port"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
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 (
|
@server (
|
||||||
@ -60,8 +68,8 @@ service ppanel {
|
|||||||
@handler QuerySubscribeList
|
@handler QuerySubscribeList
|
||||||
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
|
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
|
||||||
|
|
||||||
@doc "Get user subscribe node info"
|
@doc "Get user subscribe node info"
|
||||||
@handler QueryUserSubscribeNodeList
|
@handler QueryUserSubscribeNodeList
|
||||||
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
get /node/list returns (QueryUserSubscribeNodeListResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -66,6 +66,7 @@ type (
|
|||||||
UnbindOAuthRequest {
|
UnbindOAuthRequest {
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetLoginLogRequest {
|
GetLoginLogRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@ -94,17 +95,21 @@ type (
|
|||||||
Email string `json:"email" validate:"required"`
|
Email string `json:"email" validate:"required"`
|
||||||
Code string `json:"code" validate:"required"`
|
Code string `json:"code" validate:"required"`
|
||||||
}
|
}
|
||||||
GetDeviceListResponse {
|
|
||||||
List []UserDevice `json:"list"`
|
GetDeviceListResponse {
|
||||||
Total int64 `json:"total"`
|
List []UserDevice `json:"list"`
|
||||||
}
|
Total int64 `json:"total"`
|
||||||
UnbindDeviceRequest {
|
}
|
||||||
Id int64 `json:"id" validate:"required"`
|
|
||||||
}
|
UnbindDeviceRequest {
|
||||||
|
Id int64 `json:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
UpdateUserSubscribeNoteRequest {
|
UpdateUserSubscribeNoteRequest {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
|
||||||
Note string `json:"note" validate:"max=500"`
|
Note string `json:"note" validate:"max=500"`
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateUserRulesRequest {
|
UpdateUserRulesRequest {
|
||||||
Rules []string `json:"rules" validate:"required"`
|
Rules []string `json:"rules" validate:"required"`
|
||||||
}
|
}
|
||||||
@ -130,6 +135,23 @@ type (
|
|||||||
List []WithdrawalLog `json:"list"`
|
List []WithdrawalLog `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
GetDeviceOnlineStatsResponse {
|
||||||
|
WeeklyStats []WeeklyStat `json:"weekly_stats"`
|
||||||
|
ConnectionRecords ConnectionRecords `json:"connection_records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
WeeklyStat {
|
||||||
|
Day int `json:"day"`
|
||||||
|
DayName string `json:"day_name"`
|
||||||
|
Hours float64 `json:"hours"`
|
||||||
|
}
|
||||||
|
ConnectionRecords {
|
||||||
|
CurrentContinuousDays int64 `json:"current_continuous_days"`
|
||||||
|
HistoryContinuousDays int64 `json:"history_continuous_days"`
|
||||||
|
LongestSingleConnection int64 `json:"longest_single_connection"`
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@server (
|
@server (
|
||||||
@ -226,9 +248,9 @@ service ppanel {
|
|||||||
@handler UpdateBindEmail
|
@handler UpdateBindEmail
|
||||||
put /bind_email (UpdateBindEmailRequest)
|
put /bind_email (UpdateBindEmailRequest)
|
||||||
|
|
||||||
@doc "Get Device List"
|
@doc "Get Device List"
|
||||||
@handler GetDeviceList
|
@handler GetDeviceList
|
||||||
get /devices returns (GetDeviceListResponse)
|
get /devices returns (GetDeviceListResponse)
|
||||||
|
|
||||||
@doc "Unbind Device"
|
@doc "Unbind Device"
|
||||||
@handler UnbindDevice
|
@handler UnbindDevice
|
||||||
@ -249,5 +271,24 @@ service ppanel {
|
|||||||
@doc "Query Withdrawal Log"
|
@doc "Query Withdrawal Log"
|
||||||
@handler QueryWithdrawalLog
|
@handler QueryWithdrawalLog
|
||||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||||
}
|
|
||||||
|
|
||||||
|
@doc "Device Online Statistics"
|
||||||
|
@handler DeviceOnlineStatistics
|
||||||
|
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
|
||||||
|
|
||||||
|
@doc "Delete Current User Account"
|
||||||
|
@handler DeleteCurrentUserAccount
|
||||||
|
delete /current_user_account
|
||||||
|
|
||||||
|
}
|
||||||
|
@server(
|
||||||
|
prefix: v1/public/user
|
||||||
|
group: public/user/ws
|
||||||
|
middleware: AuthMiddleware
|
||||||
|
)
|
||||||
|
|
||||||
|
service ppanel {
|
||||||
|
@doc "Webosocket Device Connect"
|
||||||
|
@handler DeviceWsConnect
|
||||||
|
get /device_ws_connect
|
||||||
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ type (
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||||
|
IsDel bool `json:"is_del,omitempty"`
|
||||||
}
|
}
|
||||||
Follow {
|
Follow {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
@ -150,6 +151,7 @@ type (
|
|||||||
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
||||||
IpRegisterLimit int64 `json:"ip_register_limit"`
|
IpRegisterLimit int64 `json:"ip_register_limit"`
|
||||||
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
||||||
|
DeviceLimit int64 `json:"device_limit"`
|
||||||
}
|
}
|
||||||
VerifyConfig {
|
VerifyConfig {
|
||||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||||
@ -204,7 +206,7 @@ type (
|
|||||||
CurrencySymbol string `json:"currency_symbol"`
|
CurrencySymbol string `json:"currency_symbol"`
|
||||||
}
|
}
|
||||||
SubscribeDiscount {
|
SubscribeDiscount {
|
||||||
Quantity int64 `json:"quantity"`
|
Quantity int64 `json:"quantity"`
|
||||||
Discount float64 `json:"discount"`
|
Discount float64 `json:"discount"`
|
||||||
}
|
}
|
||||||
Subscribe {
|
Subscribe {
|
||||||
@ -446,6 +448,28 @@ type (
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
RedemptionCode {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
TotalCount int64 `json:"total_count"`
|
||||||
|
UsedCount int64 `json:"used_count"`
|
||||||
|
SubscribePlan int64 `json:"subscribe_plan"`
|
||||||
|
UnitTime string `json:"unit_time"`
|
||||||
|
Quantity int64 `json:"quantity"`
|
||||||
|
Status int64 `json:"status"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
|
RedemptionRecord {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
RedemptionCodeId int64 `json:"redemption_code_id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
|
UnitTime string `json:"unit_time"`
|
||||||
|
Quantity int64 `json:"quantity"`
|
||||||
|
RedeemedAt int64 `json:"redeemed_at"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
Announcement {
|
Announcement {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@ -656,7 +680,7 @@ type (
|
|||||||
// public announcement
|
// public announcement
|
||||||
QueryAnnouncementRequest {
|
QueryAnnouncementRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size,default=15"`
|
||||||
Pinned *bool `form:"pinned"`
|
Pinned *bool `form:"pinned"`
|
||||||
Popup *bool `form:"popup"`
|
Popup *bool `form:"popup"`
|
||||||
}
|
}
|
||||||
@ -673,6 +697,7 @@ type (
|
|||||||
List []SubscribeGroup `json:"list"`
|
List []SubscribeGroup `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
GetUserSubscribeTrafficLogsRequest {
|
GetUserSubscribeTrafficLogsRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
|
|||||||
BIN
cache/GeoLite2-City.mmdb
vendored
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/jinzhu/copier v0.4.0
|
||||||
github.com/klauspost/compress v1.17.7
|
github.com/klauspost/compress v1.17.7
|
||||||
github.com/nyaruka/phonenumbers v1.5.0
|
github.com/nyaruka/phonenumbers v1.5.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/redis/go-redis/v9 v9.7.2
|
github.com/redis/go-redis/v9 v9.7.2
|
||||||
github.com/smartwalle/alipay/v3 v3.2.23
|
github.com/smartwalle/alipay/v3 v3.2.23
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
|
|||||||
@ -1 +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
|
// SessionIdKey cache session key
|
||||||
const SessionIdKey = "auth:session_id"
|
const SessionIdKey = "auth:session_id"
|
||||||
|
|
||||||
|
// DeviceCacheKeyKey cache session key
|
||||||
|
const DeviceCacheKeyKey = "auth:device_identifier"
|
||||||
|
|
||||||
// GlobalConfigKey Global Config Key
|
// GlobalConfigKey Global Config Key
|
||||||
const GlobalConfigKey = "system:global_config"
|
const GlobalConfigKey = "system:global_config"
|
||||||
|
|
||||||
@ -59,3 +62,5 @@ const SendIntervalKeyPrefix = "send:interval:"
|
|||||||
|
|
||||||
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
|
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
|
||||||
const SendCountLimitKeyPrefix = "send:limit:"
|
const SendCountLimitKeyPrefix = "send:limit:"
|
||||||
|
|
||||||
|
const RegisterIpKeyPrefix = "register:ip:"
|
||||||
|
|||||||
@ -37,9 +37,18 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RedisConfig struct {
|
type RedisConfig struct {
|
||||||
Host string `yaml:"Host" default:"localhost:6379"`
|
Host string `yaml:"Host" default:"localhost:6379"`
|
||||||
Pass string `yaml:"Pass" default:""`
|
Pass string `yaml:"Pass" default:""`
|
||||||
DB int `yaml:"DB" default:"0"`
|
DB int `yaml:"DB" default:"0"`
|
||||||
|
PoolSize int `yaml:"PoolSize" default:"100"` // 连接池大小(最大连接数)
|
||||||
|
MinIdleConns int `yaml:"MinIdleConns" default:"10"` // 最小空闲连接数
|
||||||
|
MaxRetries int `yaml:"MaxRetries" default:"3"` // 最大重试次数
|
||||||
|
PoolTimeout int `yaml:"PoolTimeout" default:"4"` // 连接池超时时间(秒)
|
||||||
|
IdleTimeout int `yaml:"IdleTimeout" default:"300"` // 空闲连接超时时间(秒)
|
||||||
|
MaxConnAge int `yaml:"MaxConnAge" default:"0"` // 连接最大生命周期(秒),0表示不限制
|
||||||
|
DialTimeout int `yaml:"DialTimeout" default:"5"` // 连接超时时间(秒)
|
||||||
|
ReadTimeout int `yaml:"ReadTimeout" default:"3"` // 读超时时间(秒)
|
||||||
|
WriteTimeout int `yaml:"WriteTimeout" default:"3"` // 写超时时间(秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
type JwtAuth struct {
|
type JwtAuth struct {
|
||||||
@ -73,6 +82,7 @@ type RegisterConfig struct {
|
|||||||
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
||||||
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
||||||
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
||||||
|
DeviceLimit int64 `yaml:"DeviceLimit" default:"5"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
|
|||||||
@ -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"
|
adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing"
|
||||||
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
|
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
|
||||||
adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment"
|
adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment"
|
||||||
|
adminRedemption "github.com/perfect-panel/server/internal/handler/admin/redemption"
|
||||||
adminServer "github.com/perfect-panel/server/internal/handler/admin/server"
|
adminServer "github.com/perfect-panel/server/internal/handler/admin/server"
|
||||||
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
|
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
|
||||||
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system"
|
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system"
|
||||||
@ -30,9 +31,11 @@ import (
|
|||||||
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
|
||||||
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
|
||||||
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
|
||||||
|
publicRedemption "github.com/perfect-panel/server/internal/handler/public/redemption"
|
||||||
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
|
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
|
||||||
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
|
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
|
||||||
publicUser "github.com/perfect-panel/server/internal/handler/public/user"
|
publicUser "github.com/perfect-panel/server/internal/handler/public/user"
|
||||||
|
publicUserWs "github.com/perfect-panel/server/internal/handler/public/user/ws"
|
||||||
server "github.com/perfect-panel/server/internal/handler/server"
|
server "github.com/perfect-panel/server/internal/handler/server"
|
||||||
"github.com/perfect-panel/server/internal/middleware"
|
"github.com/perfect-panel/server/internal/middleware"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@ -298,6 +301,32 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx))
|
adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adminRedemptionGroupRouter := router.Group("/v1/admin/redemption")
|
||||||
|
adminRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||||
|
|
||||||
|
{
|
||||||
|
// Create redemption code
|
||||||
|
adminRedemptionGroupRouter.POST("/code", adminRedemption.CreateRedemptionCodeHandler(serverCtx))
|
||||||
|
|
||||||
|
// Update redemption code
|
||||||
|
adminRedemptionGroupRouter.PUT("/code", adminRedemption.UpdateRedemptionCodeHandler(serverCtx))
|
||||||
|
|
||||||
|
// Delete redemption code
|
||||||
|
adminRedemptionGroupRouter.DELETE("/code", adminRedemption.DeleteRedemptionCodeHandler(serverCtx))
|
||||||
|
|
||||||
|
// Batch delete redemption code
|
||||||
|
adminRedemptionGroupRouter.DELETE("/code/batch", adminRedemption.BatchDeleteRedemptionCodeHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get redemption code list
|
||||||
|
adminRedemptionGroupRouter.GET("/code/list", adminRedemption.GetRedemptionCodeListHandler(serverCtx))
|
||||||
|
|
||||||
|
// Toggle redemption code status
|
||||||
|
adminRedemptionGroupRouter.PUT("/code/status", adminRedemption.ToggleRedemptionCodeStatusHandler(serverCtx))
|
||||||
|
|
||||||
|
// Get redemption record list
|
||||||
|
adminRedemptionGroupRouter.GET("/record/list", adminRedemption.GetRedemptionRecordListHandler(serverCtx))
|
||||||
|
}
|
||||||
|
|
||||||
adminServerGroupRouter := router.Group("/v1/admin/server")
|
adminServerGroupRouter := router.Group("/v1/admin/server")
|
||||||
adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -748,6 +777,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx))
|
publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicRedemptionGroupRouter := router.Group("/v1/public/redemption")
|
||||||
|
publicRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||||
|
|
||||||
|
{
|
||||||
|
// Redeem code
|
||||||
|
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
|
||||||
|
}
|
||||||
|
|
||||||
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
|
||||||
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -813,6 +850,12 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Commission Withdraw
|
// Commission Withdraw
|
||||||
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
||||||
|
|
||||||
|
// Delete Current User Account
|
||||||
|
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
||||||
|
|
||||||
|
// Device Online Statistics
|
||||||
|
publicUserGroupRouter.GET("/device_online_statistics", publicUser.DeviceOnlineStatisticsHandler(serverCtx))
|
||||||
|
|
||||||
// Get Device List
|
// Get Device List
|
||||||
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
|
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
|
||||||
|
|
||||||
@ -868,6 +911,14 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
publicUserWsGroupRouter := router.Group("/v1/public/user")
|
||||||
|
publicUserWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
|
||||||
|
|
||||||
|
{
|
||||||
|
// Webosocket Device Connect
|
||||||
|
publicUserWsGroupRouter.GET("/device_ws_connect", publicUserWs.DeviceWsConnectHandler(serverCtx))
|
||||||
|
}
|
||||||
|
|
||||||
serverGroupRouter := router.Group("/v1/server")
|
serverGroupRouter := router.Group("/v1/server")
|
||||||
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))
|
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))
|
||||||
|
|
||||||
@ -888,10 +939,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
serverGroupRouter := router.Group("/v2/server")
|
serverV2GroupRouter := router.Group("/v2/server")
|
||||||
|
|
||||||
{
|
{
|
||||||
// Get Server Protocol Config
|
// 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 {
|
} else {
|
||||||
data.City = result.City
|
data.City = result.City
|
||||||
data.Country = result.Country
|
data.Country = result.Country
|
||||||
|
data.Latitude = result.Latitude
|
||||||
|
data.Longitude = result.Longitude
|
||||||
|
data.LatitudeCenter = result.LatitudeCenter
|
||||||
|
data.LongitudeCenter = result.LongitudeCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data)
|
err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data)
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
|
|||||||
data.Country = req.Country
|
data.Country = req.Country
|
||||||
data.City = req.City
|
data.City = req.City
|
||||||
// only update address when it's different
|
// only update address when it's different
|
||||||
if req.Address != data.Address {
|
if req.Address != data.Address || (data.Country == "" || req.Country == "") {
|
||||||
// query server ip location
|
// query server ip location
|
||||||
result, err := ip.GetRegionByIp(req.Address)
|
result, err := ip.GetRegionByIp(req.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -47,6 +47,10 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
|
|||||||
} else {
|
} else {
|
||||||
data.City = result.City
|
data.City = result.City
|
||||||
data.Country = result.Country
|
data.Country = result.Country
|
||||||
|
data.Latitude = result.Latitude
|
||||||
|
data.Longitude = result.Longitude
|
||||||
|
data.LatitudeCenter = result.LatitudeCenter
|
||||||
|
data.LongitudeCenter = result.LongitudeCenter
|
||||||
}
|
}
|
||||||
// update address
|
// update address
|
||||||
data.Address = req.Address
|
data.Address = req.Address
|
||||||
|
|||||||
@ -2,14 +2,12 @@ package system
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetModuleConfigLogic struct {
|
type GetModuleConfigLogic struct {
|
||||||
@ -28,14 +26,14 @@ func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *G
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
|
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
|
||||||
value, exists := os.LookupEnv("SECRET_KEY")
|
//value, exists := os.LookupEnv("SECRET_KEY")
|
||||||
if !exists {
|
//if !exists {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
|
||||||
}
|
//}
|
||||||
|
|
||||||
return &types.ModuleConfig{
|
return &types.ModuleConfig{
|
||||||
Secret: value,
|
//Secret: value,
|
||||||
ServiceName: constant.ServiceName,
|
ServiceName: constant.ServiceName,
|
||||||
ServiceVersion: constant.Version,
|
ServiceVersion: strings.ReplaceAll(constant.Version, "v", ""),
|
||||||
}, nil
|
}, 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))
|
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear server cache: %v", err.Error())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
|||||||
Unscoped: req.Unscoped,
|
Unscoped: req.Unscoped,
|
||||||
SubscribeId: req.SubscribeId,
|
SubscribeId: req.SubscribeId,
|
||||||
UserSubscribeId: req.UserSubscribeId,
|
UserSubscribeId: req.UserSubscribeId,
|
||||||
|
ShortCode: req.ShortCode,
|
||||||
Order: "DESC",
|
Order: "DESC",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -69,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))
|
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear subscribe cache: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear server cache: %v", err.Error())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@ -88,6 +89,36 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
|||||||
logger.Field("user_id", userId),
|
logger.Field("user_id", userId),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check device limit
|
||||||
|
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
|
||||||
|
if deviceLimit > 0 {
|
||||||
|
// Count current user's devices
|
||||||
|
var deviceCount int64
|
||||||
|
if err := l.svcCtx.DB.Model(&user.Device{}).Where("user_id = ?", userId).Count(&deviceCount).Error; err != nil {
|
||||||
|
l.Errorw("failed to count user devices",
|
||||||
|
logger.Field("user_id", userId),
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
)
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count devices failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if limit reached
|
||||||
|
if deviceCount >= deviceLimit {
|
||||||
|
l.Errorw("device limit reached",
|
||||||
|
logger.Field("user_id", userId),
|
||||||
|
logger.Field("device_count", deviceCount),
|
||||||
|
logger.Field("device_limit", deviceLimit),
|
||||||
|
)
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device limit reached: maximum %d devices allowed", deviceLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Infow("device limit check passed",
|
||||||
|
logger.Field("user_id", userId),
|
||||||
|
logger.Field("device_count", deviceCount),
|
||||||
|
logger.Field("device_limit", deviceLimit),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
// Create device auth method
|
// Create device auth method
|
||||||
authMethod := &user.AuthMethods{
|
authMethod := &user.AuthMethods{
|
||||||
@ -107,8 +138,9 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
|
|||||||
|
|
||||||
// Create device record
|
// Create device record
|
||||||
deviceInfo := &user.Device{
|
deviceInfo := &user.Device{
|
||||||
Ip: ip,
|
Ip: ip,
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
|
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
Identifier: identifier,
|
Identifier: identifier,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
@ -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 {
|
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
|
||||||
oldUserId := deviceInfo.UserId
|
oldUserId := deviceInfo.UserId
|
||||||
|
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
// Check device limit for new user
|
||||||
// Check if old user has other auth methods besides device
|
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
|
||||||
var authMethods []user.AuthMethods
|
if deviceLimit > 0 {
|
||||||
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
|
// 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",
|
l.Errorw("failed to query auth methods for old user",
|
||||||
logger.Field("old_user_id", oldUserId),
|
logger.Field("old_user_id", oldUserId),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
@ -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)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count non-device auth methods
|
//如果没有其他认证方式,禁用旧用户账号
|
||||||
nonDeviceAuthCount := 0
|
if count < 1 {
|
||||||
for _, auth := range authMethods {
|
//检查设备下是否有套餐,有套餐。就检查即将绑定过去的所有账户是否有套餐,如果有,那么检查两个套餐是否一致。如果一致就将即将删除的用户套餐,时间叠加到我绑定过去的用户套餐上面(如果套餐已过期就忽略)。新绑定设备的账户上套餐不一致或者不存在直接将套餐换绑即可
|
||||||
if auth.AuthType != "device" {
|
var oldUserSubscribes []user.Subscribe
|
||||||
nonDeviceAuthCount++
|
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 len(oldUserSubscribes) > 0 {
|
||||||
if nonDeviceAuthCount == 0 {
|
l.Infow("processing old user subscribes",
|
||||||
falseVal := false
|
logger.Field("old_user_id", oldUserId),
|
||||||
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
|
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",
|
l.Errorw("failed to disable old user",
|
||||||
logger.Field("old_user_id", oldUserId),
|
logger.Field("old_user_id", oldUserId),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infow("disabled old user (no other auth methods)",
|
|
||||||
logger.Field("old_user_id", oldUserId),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
l.Infow("old user has other auth methods, not disabling",
|
|
||||||
logger.Field("old_user_id", oldUserId),
|
|
||||||
logger.Field("non_device_auth_count", nonDeviceAuthCount),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update device auth method to new user
|
l.Infow("disabled old user (no other auth methods)",
|
||||||
if err := db.Model(&user.AuthMethods{}).
|
logger.Field("old_user_id", oldUserId),
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update device record
|
// 更新设备绑定的用户id
|
||||||
deviceInfo.UserId = newUserId
|
deviceInfo.UserId = newUserId
|
||||||
deviceInfo.Ip = ip
|
deviceInfo.Ip = ip
|
||||||
deviceInfo.UserAgent = userAgent
|
deviceInfo.UserAgent = userAgent
|
||||||
deviceInfo.Enabled = true
|
deviceInfo.Enabled = true
|
||||||
|
if err := tx.Save(deviceInfo).Error; err != nil {
|
||||||
if err := db.Save(deviceInfo).Error; err != nil {
|
|
||||||
l.Errorw("failed to update device",
|
l.Errorw("failed to update device",
|
||||||
logger.Field("identifier", deviceInfo.Identifier),
|
logger.Field("identifier", deviceInfo.Identifier),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -224,6 +386,15 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = l.svcCtx.UserModel.ClearUserCache(l.ctx, users...)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("failed to clear user cache after rebinding",
|
||||||
|
logger.Field("old_user_id", oldUserId),
|
||||||
|
logger.Field("new_user_id", newUserId),
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
l.Infow("device rebound successfully",
|
l.Infow("device rebound successfully",
|
||||||
logger.Field("identifier", deviceInfo.Identifier),
|
logger.Field("identifier", deviceInfo.Identifier),
|
||||||
logger.Field("old_user_id", oldUserId),
|
logger.Field("old_user_id", oldUserId),
|
||||||
|
|||||||
@ -71,6 +71,9 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
|||||||
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "device", req.Identifier) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
|
||||||
|
}
|
||||||
// Device not found, create new user and device
|
// Device not found, create new user and device
|
||||||
userInfo, err = l.registerUserAndDevice(req)
|
userInfo, err = l.registerUserAndDevice(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -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())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store device id in redis
|
||||||
|
|
||||||
|
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||||
|
if err = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||||
|
l.Errorw("set device id error",
|
||||||
|
logger.Field("user_id", userInfo.Id),
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
)
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set device id error: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
loginStatus = true
|
loginStatus = true
|
||||||
return &types.LoginResponse{
|
return &types.LoginResponse{
|
||||||
Token: token,
|
Token: token,
|
||||||
@ -138,9 +152,11 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
)
|
)
|
||||||
|
|
||||||
var userInfo *user.User
|
var userInfo *user.User
|
||||||
|
var trialSubscribe *user.Subscribe
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
// Create new user
|
// Create new user
|
||||||
userInfo = &user.User{
|
userInfo = &user.User{
|
||||||
|
Salt: "default",
|
||||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||||
}
|
}
|
||||||
if err := db.Create(userInfo).Error; err != nil {
|
if err := db.Create(userInfo).Error; err != nil {
|
||||||
@ -182,6 +198,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
UserId: userInfo.Id,
|
UserId: userInfo.Id,
|
||||||
UserAgent: req.UserAgent,
|
UserAgent: req.UserAgent,
|
||||||
Identifier: req.Identifier,
|
Identifier: req.Identifier,
|
||||||
|
ShortCode: req.ShortCode,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Online: false,
|
Online: false,
|
||||||
}
|
}
|
||||||
@ -196,8 +213,10 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
|
|
||||||
// Activate trial if enabled
|
// Activate trial if enabled
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
if l.svcCtx.Config.Register.EnableTrial {
|
||||||
if err := l.activeTrial(userInfo.Id, db); err != nil {
|
var trialErr error
|
||||||
return err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear cache after transaction success
|
||||||
|
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
||||||
|
// Clear user subscription cache
|
||||||
|
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear subscription cache
|
||||||
|
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear all server cache
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
l.Infow("device registration completed successfully",
|
l.Infow("device registration completed successfully",
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
logger.Field("identifier", req.Identifier),
|
logger.Field("identifier", req.Identifier),
|
||||||
@ -244,7 +282,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
|
|||||||
return userInfo, nil
|
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)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("failed to find trial subscription template",
|
l.Errorw("failed to find trial subscription template",
|
||||||
@ -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("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@ -279,7 +317,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
|
|||||||
logger.Field("user_id", userId),
|
logger.Field("user_id", userId),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infow("trial subscription activated successfully",
|
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),
|
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 userInfo *user.User
|
||||||
|
var trialSubscribe *user.Subscribe
|
||||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
if email != "" {
|
if email != "" {
|
||||||
l.Debugw("checking if email already exists",
|
l.Debugw("checking if email already exists",
|
||||||
@ -397,8 +398,10 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
|||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
)
|
)
|
||||||
if err := l.activeTrial(userInfo.Id, requestID); err != nil {
|
var trialErr error
|
||||||
return err
|
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
|
return userInfo, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear cache after transaction success
|
||||||
|
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
||||||
|
// Clear user subscription cache
|
||||||
|
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear subscription cache
|
||||||
|
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear all server cache
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
l.Infow("user registration completed successfully",
|
l.Infow("user registration completed successfully",
|
||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
@ -793,7 +815,7 @@ func (l *OAuthLoginGetTokenLogic) findOrRegisterUser(authType, openID, email, av
|
|||||||
return userInfo, nil
|
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",
|
l.Debugw("fetching trial subscription template",
|
||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", uid),
|
logger.Field("user_id", uid),
|
||||||
@ -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("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@ -848,7 +870,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error
|
|||||||
logger.Field("user_id", uid),
|
logger.Field("user_id", uid),
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
)
|
)
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Infow("trial subscription activated successfully",
|
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("expire_time", expireTime),
|
||||||
logger.Field("traffic", sub.Traffic),
|
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
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -133,7 +133,8 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("identifier", req.Identifier),
|
||||||
|
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/perfect-panel/server/internal/logic/common"
|
"github.com/perfect-panel/server/internal/logic/common"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
@ -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")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
||||||
}
|
}
|
||||||
loginStatus := false
|
loginStatus := false
|
||||||
var userInfo *user.User
|
|
||||||
|
authMethodInfo, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber)
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, authMethodInfo.UserId)
|
||||||
|
if err != nil {
|
||||||
|
if errors.As(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
||||||
|
}
|
||||||
// Record login status
|
// Record login status
|
||||||
defer func(svcCtx *svc.ServiceContext) {
|
defer func(svcCtx *svc.ServiceContext) {
|
||||||
if userInfo.Id != 0 {
|
if userInfo.Id != 0 {
|
||||||
@ -76,22 +90,6 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
}
|
}
|
||||||
}(l.svcCtx)
|
}(l.svcCtx)
|
||||||
|
|
||||||
authMethodInfo, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber)
|
|
||||||
if err != nil {
|
|
||||||
if errors.As(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethodInfo.UserId)
|
|
||||||
if err != nil {
|
|
||||||
if errors.As(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
|
|
||||||
}
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Password == "" && req.TelephoneCode == "" {
|
if req.Password == "" && req.TelephoneCode == "" {
|
||||||
return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty")
|
return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty")
|
||||||
}
|
}
|
||||||
@ -137,8 +135,8 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate session id
|
// Generate session id
|
||||||
@ -150,7 +148,8 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("identifier", req.Identifier),
|
||||||
|
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -43,19 +44,32 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.svcCtx.Config.Mobile.Enable {
|
if !l.svcCtx.Config.Mobile.Enable {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the email verification is enabled, the verification code is required
|
// if the email verification is enabled, the verification code is required
|
||||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.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()
|
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
}
|
}
|
||||||
|
l.Logger.Infof("TelephoneResetPassword cacheKey: %s, code: %s,value : %s", cacheKey, code, value)
|
||||||
|
if value == "" {
|
||||||
|
l.Errorf("TelephoneResetPassword value empty: %s", value)
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
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
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -108,7 +122,8 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("identifier", req.Identifier),
|
||||||
|
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -45,6 +45,7 @@ func NewTelephoneUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceConte
|
|||||||
|
|
||||||
func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
|
func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
|
||||||
c := l.svcCtx.Config.Register
|
c := l.svcCtx.Config.Register
|
||||||
|
var trialSubscribe *user.Subscribe
|
||||||
// Check if the registration is stopped
|
// Check if the registration is stopped
|
||||||
if c.StopRegister {
|
if c.StopRegister {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
||||||
@ -102,7 +103,9 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "mobile", phoneNumber) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
|
||||||
|
}
|
||||||
// Generate password
|
// Generate password
|
||||||
pwd := tool.EncodePassWord(req.Password)
|
pwd := tool.EncodePassWord(req.Password)
|
||||||
userInfo := &user.User{
|
userInfo := &user.User{
|
||||||
@ -133,12 +136,36 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
}
|
}
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
if l.svcCtx.Config.Register.EnableTrial {
|
||||||
// Active trial
|
// Active trial
|
||||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
var trialErr error
|
||||||
return err
|
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
|
||||||
|
if trialErr != nil {
|
||||||
|
return trialErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache after transaction success
|
||||||
|
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
||||||
|
// Clear user subscription cache
|
||||||
|
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear subscription cache
|
||||||
|
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear all server cache
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
@ -152,8 +179,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
// Don't fail register if device binding fails, just log the error
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -164,7 +191,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("identifier", req.Identifier),
|
||||||
|
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
@ -226,10 +254,10 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
|
|||||||
}, nil
|
}, 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)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
userSub := &user.Subscribe{
|
userSub := &user.Subscribe{
|
||||||
Id: 0,
|
Id: 0,
|
||||||
@ -245,5 +273,10 @@ func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) error {
|
|||||||
UUID: uuidx.NewUUID().String(),
|
UUID: uuidx.NewUUID().String(),
|
||||||
Status: 1,
|
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
|
// Don't fail login if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -109,7 +109,8 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
|||||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||||
jwt.WithOption("UserId", userInfo.Id),
|
jwt.WithOption("UserId", userInfo.Id),
|
||||||
jwt.WithOption("SessionId", sessionId),
|
jwt.WithOption("SessionId", sessionId),
|
||||||
jwt.WithOption("LoginType", req.LoginType),
|
jwt.WithOption("identifier", req.Identifier),
|
||||||
|
jwt.WithOption("CtxLoginType", req.LoginType),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -42,6 +42,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
c := l.svcCtx.Config.Register
|
c := l.svcCtx.Config.Register
|
||||||
email := l.svcCtx.Config.Email
|
email := l.svcCtx.Config.Email
|
||||||
var referer *user.User
|
var referer *user.User
|
||||||
|
var trialSubscribe *user.Subscribe
|
||||||
// Check if the registration is stopped
|
// Check if the registration is stopped
|
||||||
if c.StopRegister {
|
if c.StopRegister {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
|
||||||
@ -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)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserDisabled), "user email deleted: %v", req.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "email", req.Email) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
|
||||||
|
}
|
||||||
|
|
||||||
// Generate password
|
// Generate password
|
||||||
pwd := tool.EncodePassWord(req.Password)
|
pwd := tool.EncodePassWord(req.Password)
|
||||||
userInfo := &user.User{
|
userInfo := &user.User{
|
||||||
@ -123,12 +128,36 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
if l.svcCtx.Config.Register.EnableTrial {
|
||||||
// Active trial
|
// Active trial
|
||||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
var trialErr error
|
||||||
return err
|
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
|
||||||
|
if trialErr != nil {
|
||||||
|
return trialErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cache after transaction success
|
||||||
|
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
|
||||||
|
// Clear user subscription cache
|
||||||
|
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear subscription cache
|
||||||
|
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
|
||||||
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
// Clear all server cache
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
// Don't return error, just log it
|
||||||
|
}
|
||||||
|
}
|
||||||
// Bind device to user if identifier is provided
|
// Bind device to user if identifier is provided
|
||||||
if req.Identifier != "" {
|
if req.Identifier != "" {
|
||||||
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||||
@ -141,8 +170,8 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
// Don't fail register if device binding fails, just log the error
|
// Don't fail register if device binding fails, just log the error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if l.ctx.Value(constant.LoginType) != nil {
|
if l.ctx.Value(constant.CtxLoginType) != nil {
|
||||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
|
||||||
}
|
}
|
||||||
// Generate session id
|
// Generate session id
|
||||||
sessionId := uuidx.NewUUID().String()
|
sessionId := uuidx.NewUUID().String()
|
||||||
@ -216,10 +245,10 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
}, nil
|
}, 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)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
userSub := &user.Subscribe{
|
userSub := &user.Subscribe{
|
||||||
UserId: uid,
|
UserId: uid,
|
||||||
@ -234,5 +263,8 @@ func (l *UserRegisterLogic) activeTrial(uid int64) error {
|
|||||||
UUID: uuidx.NewUUID().String(),
|
UUID: uuidx.NewUUID().String(),
|
||||||
Status: 1,
|
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()))
|
l.Logger.Error("[AlipayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
||||||
return err
|
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)
|
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -84,7 +84,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
|||||||
l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error()))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
||||||
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@ -85,7 +85,7 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
|
|||||||
l.Errorw("[StripeNotify] Marshal error", logger.Field("errors", err.Error()), logger.Field("payload", payload))
|
l.Errorw("[StripeNotify] Marshal error", logger.Field("errors", err.Error()), logger.Field("payload", payload))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
|
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
|
||||||
_, err = l.svcCtx.Queue.Enqueue(task)
|
_, err = l.svcCtx.Queue.Enqueue(task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))
|
||||||
|
|||||||
@ -55,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))
|
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check subscribe plan quota limit
|
||||||
|
if sub.Quota > 0 {
|
||||||
|
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
for _, v := range userSub {
|
||||||
|
if v.SubscribeId == req.SubscribeId {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count >= sub.Quota {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var discount float64 = 1
|
var discount float64 = 1
|
||||||
if sub.Discount != "" {
|
if sub.Discount != "" {
|
||||||
var dis []types.SubscribeDiscount
|
var dis []types.SubscribeDiscount
|
||||||
@ -98,18 +117,6 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
couponAmount = calculateCoupon(amount, couponInfo)
|
couponAmount = calculateCoupon(amount, couponInfo)
|
||||||
}
|
}
|
||||||
amount -= couponAmount
|
amount -= couponAmount
|
||||||
|
|
||||||
var deductionAmount int64
|
|
||||||
// Check user deduction amount
|
|
||||||
if u.GiftAmount > 0 {
|
|
||||||
if u.GiftAmount >= amount {
|
|
||||||
deductionAmount = amount
|
|
||||||
amount = 0
|
|
||||||
} else {
|
|
||||||
deductionAmount = u.GiftAmount
|
|
||||||
amount -= u.GiftAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var feeAmount int64
|
var feeAmount int64
|
||||||
if req.Payment != 0 {
|
if req.Payment != 0 {
|
||||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
@ -120,8 +127,19 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
|||||||
// Calculate the handling fee
|
// Calculate the handling fee
|
||||||
if amount > 0 {
|
if amount > 0 {
|
||||||
feeAmount = calculateFee(amount, payment)
|
feeAmount = calculateFee(amount, payment)
|
||||||
|
amount += feeAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Calculate gift amount deduction after fee calculation
|
||||||
|
var deductionAmount int64
|
||||||
|
if u.GiftAmount > 0 && amount > 0 {
|
||||||
|
if u.GiftAmount >= amount {
|
||||||
|
deductionAmount = amount
|
||||||
|
amount = 0
|
||||||
|
} else {
|
||||||
|
deductionAmount = u.GiftAmount
|
||||||
|
amount -= u.GiftAmount
|
||||||
}
|
}
|
||||||
amount += feeAmount
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = &types.PreOrderResponse{
|
resp = &types.PreOrderResponse{
|
||||||
|
|||||||
@ -93,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")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check subscribe plan limit
|
|
||||||
if sub.Quota > 0 {
|
|
||||||
var count int64
|
|
||||||
for _, v := range userSub {
|
|
||||||
if v.SubscribeId == req.SubscribeId {
|
|
||||||
count += 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count >= sub.Quota {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var discount float64 = 1
|
var discount float64 = 1
|
||||||
if sub.Discount != "" {
|
if sub.Discount != "" {
|
||||||
var dis []types.SubscribeDiscount
|
var dis []types.SubscribeDiscount
|
||||||
@ -160,19 +147,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
// Calculate the handling fee
|
// Calculate the handling fee
|
||||||
amount -= coupon
|
amount -= coupon
|
||||||
var deductionAmount int64
|
|
||||||
// Check user deduction amount
|
|
||||||
if u.GiftAmount > 0 {
|
|
||||||
if u.GiftAmount >= amount {
|
|
||||||
deductionAmount = amount
|
|
||||||
amount = 0
|
|
||||||
u.GiftAmount -= deductionAmount
|
|
||||||
} else {
|
|
||||||
deductionAmount = u.GiftAmount
|
|
||||||
amount -= u.GiftAmount
|
|
||||||
u.GiftAmount = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// find payment method
|
// find payment method
|
||||||
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -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")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Calculate gift amount deduction after fee calculation
|
||||||
|
var deductionAmount int64
|
||||||
|
if u.GiftAmount > 0 && amount > 0 {
|
||||||
|
if u.GiftAmount >= amount {
|
||||||
|
deductionAmount = amount
|
||||||
|
amount = 0
|
||||||
|
} else {
|
||||||
|
deductionAmount = u.GiftAmount
|
||||||
|
amount -= u.GiftAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
// query user is new purchase or renewal
|
// query user is new purchase or renewal
|
||||||
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -221,9 +206,28 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
// Database transaction
|
// Database transaction
|
||||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
||||||
// 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 {
|
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 {
|
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil {
|
||||||
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
|
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
|
||||||
return e
|
return e
|
||||||
|
|||||||
@ -366,6 +366,7 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
|
|||||||
}
|
}
|
||||||
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create payment URL for user redirection
|
// Create payment URL for user redirection
|
||||||
url := client.CreatePayUrl(epay.Order{
|
url := client.CreatePayUrl(epay.Order{
|
||||||
Name: l.svcCtx.Config.Site.SiteName,
|
Name: l.svcCtx.Config.Site.SiteName,
|
||||||
|
|||||||
@ -150,7 +150,7 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
|
|||||||
}
|
}
|
||||||
content, _ := tempOrder.Marshal()
|
content, _ := tempOrder.Marshal()
|
||||||
|
|
||||||
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), 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))
|
l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo))
|
||||||
return err
|
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")
|
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 {
|
if err != nil {
|
||||||
logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR")
|
||||||
@ -79,7 +79,6 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
|
|||||||
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
|
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
|
||||||
userSubscribeInfo.IsTryOut = true
|
userSubscribeInfo.IsTryOut = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.List = append(resp.List, userSubscribeInfo)
|
resp.List = append(resp.List, userSubscribeInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,16 +136,21 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
userSubscribeNode := &types.UserSubscribeNodeInfo{
|
||||||
Id: n.Id,
|
Id: n.Id,
|
||||||
Name: n.Name,
|
Name: n.Name,
|
||||||
Uuid: userSub.UUID,
|
Uuid: userSub.UUID,
|
||||||
Protocol: n.Protocol,
|
Protocol: n.Protocol,
|
||||||
Port: n.Port,
|
Protocols: server.Protocols,
|
||||||
Address: n.Address,
|
Port: n.Port,
|
||||||
Tags: strings.Split(n.Tags, ","),
|
Address: n.Address,
|
||||||
Country: server.Country,
|
Tags: strings.Split(n.Tags, ","),
|
||||||
City: server.City,
|
Country: server.Country,
|
||||||
CreatedAt: n.CreatedAt.Unix(),
|
City: server.City,
|
||||||
|
Latitude: server.Latitude,
|
||||||
|
Longitude: server.Longitude,
|
||||||
|
LongitudeCenter: server.LongitudeCenter,
|
||||||
|
LatitudeCenter: server.LatitudeCenter,
|
||||||
|
CreatedAt: n.CreatedAt.Unix(),
|
||||||
}
|
}
|
||||||
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
|
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))
|
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "ClearSubscribeCache failed: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
||||||
|
l.Errorf("ClearServerAllCache error: %v", err.Error())
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "failed to clear server cache: %v", err.Error())
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -64,9 +64,23 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
|
||||||
}
|
}
|
||||||
sessionId := l.ctx.Value(constant.CtxKeySessionID)
|
var count int64
|
||||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
err = tx.Model(user.AuthMethods{}).Where("user_id = ?", deleteDevice.UserId).Count(&count).Error
|
||||||
l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey)
|
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
|
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 := ""
|
loginType := ""
|
||||||
if claims["LoginType"] != nil {
|
if claims["CtxLoginType"] != nil {
|
||||||
loginType = claims["LoginType"].(string)
|
loginType = claims["CtxLoginType"].(string)
|
||||||
|
}
|
||||||
|
if claims["identifier"] != nil {
|
||||||
|
ctx = context.WithValue(ctx, constant.CtxKeyIdentifier, claims["identifier"].(string))
|
||||||
}
|
}
|
||||||
// get user id from token
|
// get user id from token
|
||||||
userId := int64(claims["UserId"].(float64))
|
userId := int64(claims["UserId"].(float64))
|
||||||
@ -82,9 +85,10 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
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.CtxKeyUser, userInfo)
|
||||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
||||||
|
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,11 +42,11 @@ func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
|
|||||||
|
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
|
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
|
||||||
ctx = context.WithValue(ctx, constant.LoginType, c.GetHeader("Login-Type"))
|
ctx = context.WithValue(ctx, constant.CtxLoginType, c.GetHeader("Login-Type"))
|
||||||
c.Request = c.Request.WithContext(ctx)
|
c.Request = c.Request.WithContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
loginType, ok := ctx.Value(constant.LoginType).(string)
|
loginType, ok := ctx.Value(constant.CtxLoginType).(string)
|
||||||
if !ok || loginType != "device" {
|
if !ok || loginType != "device" {
|
||||||
c.Next()
|
c.Next()
|
||||||
return
|
return
|
||||||
|
|||||||
@ -27,6 +27,9 @@ type Filter struct {
|
|||||||
|
|
||||||
// GetAnnouncementListByPage get announcement list by page
|
// GetAnnouncementListByPage get announcement list by page
|
||||||
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
|
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
|
||||||
|
if size == 0 {
|
||||||
|
size = 10
|
||||||
|
}
|
||||||
var list []*Announcement
|
var list []*Announcement
|
||||||
var total int64
|
var total int64
|
||||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type customServerLogicModel interface {
|
|||||||
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
|
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
|
||||||
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
|
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
|
||||||
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
|
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
|
||||||
|
ClearServerAllCache(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -171,6 +172,30 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
|
||||||
|
var cursor uint64
|
||||||
|
var keys []string
|
||||||
|
prefix := ServerUserListCacheKey + "*"
|
||||||
|
for {
|
||||||
|
scanKeys, newCursor, err := m.Cache.Scan(ctx, cursor, prefix, 999).Result()
|
||||||
|
if err != nil {
|
||||||
|
m.Logger.Error(ctx, fmt.Sprintf("ClearServerAllCache err:%v", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache query keys:%v", scanKeys))
|
||||||
|
keys = append(keys, scanKeys...)
|
||||||
|
cursor = newCursor
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(keys) > 0 {
|
||||||
|
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache keys:%v", keys))
|
||||||
|
return m.Cache.Del(ctx, keys...).Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// InSet 支持多值 OR 查询
|
// InSet 支持多值 OR 查询
|
||||||
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
|||||||
@ -15,12 +15,16 @@ type Server struct {
|
|||||||
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
|
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
|
||||||
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
|
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
|
||||||
//Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
|
//Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
|
||||||
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
|
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
|
||||||
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
|
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
|
||||||
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
|
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
|
||||||
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
|
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
Longitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Longitude"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
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 {
|
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
|
UserId *int64
|
||||||
SubscribeId *int64
|
SubscribeId *int64
|
||||||
UserSubscribeId *int64
|
UserSubscribeId *int64
|
||||||
|
ShortCode string
|
||||||
Order string // Order by id, e.g., "desc"
|
Order string // Order by id, e.g., "desc"
|
||||||
Unscoped bool // Whether to include soft-deleted records
|
Unscoped bool // Whether to include soft-deleted records
|
||||||
}
|
}
|
||||||
@ -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").
|
conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
|
||||||
Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId)
|
Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId)
|
||||||
}
|
}
|
||||||
|
if filter.ShortCode != "" {
|
||||||
|
conn = conn.Joins("LEFT JOIN user_device ON user.id = user_device.user_id").
|
||||||
|
Where("user_device.short_code LIKE ?", "%"+filter.ShortCode+"%")
|
||||||
|
}
|
||||||
if filter.Order != "" {
|
if filter.Order != "" {
|
||||||
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
|
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
|
||||||
}
|
}
|
||||||
@ -153,7 +158,7 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
|
|||||||
conn = conn.Unscoped()
|
conn = conn.Unscoped()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").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
|
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) {
|
func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) {
|
||||||
var data []*User
|
var data []*User
|
||||||
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
||||||
return conn.Model(&User{}).Preload("AuthMethods").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
|
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