Compare commits

..

No commits in common. "7d46b31866bb676bb0073c717ede213a8bb511a4" and "ea94f3c9f97f2f7d6a9ca2691966de3a49d3a8ea" have entirely different histories.

114 changed files with 576 additions and 6517 deletions

View File

@ -1,27 +0,0 @@
# Production Environment Configuration for GitHub Actions
# This file defines production-specific deployment settings
environment:
name: production
url: https://api.ppanel.example.com
protection_rules:
- type: wait_timer
minutes: 5
- type: reviewers
reviewers:
- "@admin-team"
- "@devops-team"
variables:
ENVIRONMENT: production
LOG_LEVEL: info
DEPLOY_TIMEOUT: 300
# Environment-specific secrets required:
# PRODUCTION_HOST - Production server hostname/IP
# PRODUCTION_USER - SSH username for production server
# PRODUCTION_SSH_KEY - SSH private key for production server
# PRODUCTION_PORT - SSH port (default: 22)
# PRODUCTION_URL - Application URL for health checks
# DATABASE_PASSWORD - Production database password
# REDIS_PASSWORD - Production Redis password
# JWT_SECRET - JWT secret key for production

View File

@ -1,23 +0,0 @@
# Staging Environment Configuration for GitHub Actions
# This file defines staging-specific deployment settings
environment:
name: staging
url: https://staging-api.ppanel.example.com
protection_rules:
- type: wait_timer
minutes: 2
variables:
ENVIRONMENT: staging
LOG_LEVEL: debug
DEPLOY_TIMEOUT: 180
# Environment-specific secrets required:
# STAGING_HOST - Staging server hostname/IP
# STAGING_USER - SSH username for staging server
# STAGING_SSH_KEY - SSH private key for staging server
# STAGING_PORT - SSH port (default: 22)
# STAGING_URL - Application URL for health checks
# DATABASE_PASSWORD - Staging database password
# REDIS_PASSWORD - Staging Redis password
# JWT_SECRET - JWT secret key for staging

View File

@ -1,79 +0,0 @@
name: Build Linux Binary
on:
push:
branches: [ main, master ]
tags:
- 'v*'
workflow_dispatch:
inputs:
version:
description: 'Version to build (leave empty for auto)'
required: false
type: string
permissions:
contents: write
jobs:
build:
name: Build Linux Binary
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.23.3'
cache: true
- name: Build
env:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
run: |
VERSION=${{ github.event.inputs.version }}
if [ -z "$VERSION" ]; then
VERSION=$(git describe --tags --always --dirty)
fi
echo "Building ppanel-server $VERSION"
BUILD_TIME=$(date +"%Y-%m-%d_%H:%M:%S")
go build -ldflags="-w -s -X github.com/perfect-panel/server/pkg/constant.Version=$VERSION -X github.com/perfect-panel/server/pkg/constant.BuildTime=$BUILD_TIME" -o ppanel-server ./ppanel.go
tar -czf ppanel-server-${VERSION}-linux-amd64.tar.gz ppanel-server
sha256sum ppanel-server ppanel-server-${VERSION}-linux-amd64.tar.gz > checksum.txt
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ppanel-server-linux-amd64
path: |
ppanel-server
ppanel-server-*-linux-amd64.tar.gz
checksum.txt
- name: Create Release
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION=${GITHUB_REF#refs/tags/}
# Check if release exists
if gh release view $VERSION >/dev/null 2>&1; then
echo "Release $VERSION already exists, deleting old assets..."
# Delete existing assets if they exist
gh release delete-asset $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz --yes 2>/dev/null || true
gh release delete-asset $VERSION checksum.txt --yes 2>/dev/null || true
else
echo "Creating new release $VERSION..."
gh release create $VERSION --title "PPanel Server $VERSION" --notes "Release $VERSION"
fi
# Upload assets (will overwrite if --clobber is supported, otherwise will fail gracefully)
echo "Uploading assets..."
gh release upload $VERSION ppanel-server-${VERSION}-linux-amd64.tar.gz checksum.txt --clobber

51
.github/workflows/develop.yaml vendored Normal file
View File

@ -0,0 +1,51 @@
name: Deploy
on:
push:
branches: ["develop"]
pull_request:
branches: ["develop"]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get short Git commit ID
id: vars
run: echo "COMMIT_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build Docker image
run: docker build --build-arg VERSION=${{ env.COMMIT_ID }} -t ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }} .
- name: Push Docker image
run: docker push ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }}
# - name: Deploy to server
# uses: appleboy/ssh-action@v0.1.6
# with:
# host: ${{ secrets.SSH_HOST }}
# username: ${{ secrets.SSH_USER }}
# key: ${{ secrets.SSH_PRIVATE_KEY }}
# script: |
# if [ $(docker ps -a -q -f name=ppanel-server-dev) ]; then
# echo "Stopping and removing existing ppanel-server container..."
# docker stop ppanel-server-dev
# docker rm ppanel-server-dev
# else
# echo "No existing ppanel-server-dev container running."
# fi
#
# docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
# docker run -d --restart=always --log-driver=journald --name ppanel-server-dev -p 8080:8080 -v /www/wwwroot/api/etc:/app/etc -v /www/wwwroot/api/logs:/app/logs --restart=always -d ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }}
#

131
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,131 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-docker:
runs-on: ubuntu-latest
env:
IMAGE_NAME: ppanel-server
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract version from git tag
id: version
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
- name: Get short SHA
id: sha
run: echo "GIT_SHA=${GITHUB_SHA::8}" >> $GITHUB_ENV
- name: Set BUILD_TIME env
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
- name: Build and push Docker image for main release
if: "!contains(github.ref_name, 'beta')"
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
build-args: |
VERSION=${{ env.VERSION }}
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
- name: Build and push Docker image for beta release
if: contains(github.ref_name, 'beta')
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
build-args: |
VERSION=${{ env.VERSION }}
tags: |
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:beta
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
release-notes:
runs-on: ubuntu-latest
needs: build-docker
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Install GoReleaser
run: |
go install github.com/goreleaser/goreleaser/v2@latest
- name: Run GoReleaser
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
goreleaser check
goreleaser release --clean
releases-matrix:
name: Release ppanel-server binary
runs-on: ubuntu-latest
needs: release-notes # wait for release-notes job to finish
strategy:
matrix:
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
goos: [ linux, windows, darwin ]
goarch: [ '386', amd64, arm64 ]
exclude:
- goarch: '386'
goos: darwin
steps:
- uses: actions/checkout@v2
- name: Extract version from git tag
id: version
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
- name: Set BUILD_TIME env
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
- uses: actions/checkout@v4
- uses: wangyoucao577/go-release-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
asset_name: "ppanel-server-${{ matrix.goos }}-${{ matrix.goarch }}"
goversion: "https://dl.google.com/go/go1.23.3.linux-amd64.tar.gz"
project_path: "."
binary_name: "ppanel-server"
extra_files: LICENSE etc
ldflags: -X "github.com/perfect-panel/server/pkg/constant.Version=${{env.VERSION}}" -X "github.com/perfect-panel/server/pkg/constant.BuildTime=${{env.BUILD_TIME}}"

81
.github/workflows/swagger.yaml vendored Normal file
View File

@ -0,0 +1,81 @@
name: Go CI/CD with goctl and Swagger
on:
# release:
# types: [published]
push:
branches:
- develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install goctl
run: |
curl -L https://github.com/zeromicro/go-zero/releases/download/tools%2Fgoctl%2Fv1.7.2/goctl-v1.7.2-linux-amd64.tar.gz -o goctl-v1.7.2-linux-amd64.tar.gz
tar -xvzf goctl-v1.7.2-linux-amd64.tar.gz
chmod +x goctl
sudo mv goctl /usr/local/bin/goctl
goctl --version
- name: Install goctl-swagger
run: |
curl -L https://github.com/tensionc/goctl-swagger/releases/download/v1.0.1/goctl-swagger-v1.0.1-linux-amd64.tar.gz -o goctl-swagger.tar.gz
tar -xvzf goctl-swagger.tar.gz
chmod +x goctl-swagger
sudo mv goctl-swagger /usr/local/bin/
- name: Generate Swagger file
run: |
mkdir -p swagger
goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_common.api -dir ./swagger
goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_user.api -dir ./swagger
goctl api plugin -plugin goctl-swagger='swagger -filename admin.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_admin.api -dir ./swagger
goctl api plugin -plugin goctl-swagger='swagger -filename ppanel.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ppanel.api -dir ./swagger
goctl api plugin -plugin goctl-swagger='swagger -filename node.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_node.api -dir ./swagger
- name: Verify Swagger file
run: |
test -f ./swagger/common.json
test -f ./swagger/user.json
test -f ./swagger/admin.json
- name: Checkout target repository
uses: actions/checkout@v4
with:
repository: perfect-panel/ppanel-docs
token: ${{ secrets.GH_TOKEN }}
path: ppanel-docs
persist-credentials: true
- name: Verify or create public/swagger directory
run: |
mkdir -p ./ppanel-docs/public/swagger
- name: Copy Swagger files
run: |
cp -rf swagger/* ppanel-docs/public/swagger
cd ppanel-docs
- name: Check for file changes
run: |
cd ppanel-docs
git add .
git status
if [ "$(git status --porcelain)" ]; then
echo "Changes detected in the doc repository."
git config user.name "GitHub Actions"
git config user.email "actions@ppanel.dev"
git commit -m "Update Swagger files"
git push
else
echo "No changes detected."
exit 0
fi

View File

@ -1,130 +0,0 @@
version: 2
before:
hooks:
- go mod tidy
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- "386"
- amd64
- arm64
ignore:
- goos: darwin
goarch: "386"
binary: ppanel-server
ldflags:
- -s -w
- -X "github.com/perfect-panel/server/pkg/constant.Version={{.Version}}"
- -X "github.com/perfect-panel/server/pkg/constant.BuildTime={{.Date}}"
- -X "github.com/perfect-panel/server/pkg/constant.GitCommit={{.Commit}}"
main: ./ppanel.go
archives:
- format: tar.gz
name_template: >-
{{ .ProjectName }}-
{{- .Version }}-
{{- title .Os }}-
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
files:
- LICENSE
- etc/*
format_overrides:
- goos: windows
format: zip
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
use: github
filters:
exclude:
- "^docs:"
- "^test:"
- "^chore:"
- Merge pull request
groups:
- title: Features
regexp: "^.*feat[(\\w)]*:+.*$"
order: 0
- title: 'Bug fixes'
regexp: "^.*fix[(\\w)]*:+.*$"
order: 1
- title: Others
order: 999
dockers:
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}.{{ .Minor }}"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:latest"
dockerfile: Dockerfile
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--platform=linux/amd64"
use: docker
extra_files:
- etc/
- image_templates:
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}-arm64"
- "{{ .Env.DOCKER_USERNAME }}/ppanel-server:v{{ .Major }}.{{ .Minor }}-arm64"
dockerfile: Dockerfile
build_flag_templates:
- "--pull"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--platform=linux/arm64"
use: docker
goarch: arm64
extra_files:
- etc/
docker_signs:
- cmd: cosign
args:
- "sign"
- "${artifact}@${digest}"
env:
- COSIGN_EXPERIMENTAL=1
release:
github:
owner: perfect-panel
name: server
draft: false
prerelease: auto
name_template: "{{.ProjectName}} v{{.Version}}"
header: |
## ppanel-server {{.Version}}
Welcome to this new release!
footer: |
Docker images are available at:
- `{{ .Env.DOCKER_USERNAME }}/ppanel-server:{{ .Tag }}`
- `{{ .Env.DOCKER_USERNAME }}/ppanel-server:latest`
For more information, visit our documentation.

View File

@ -1,12 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="go build github.com/perfect-panel/server" type="GoApplicationRunConfiguration" factoryName="Go Application" nameIsGenerated="true">
<module name="server" />
<working_directory value="$PROJECT_DIR$" />
<parameters value="run --config etc/ppanel-dev.yaml" />
<kind value="PACKAGE" />
<package value="github.com/perfect-panel/server" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/ppanel.go" />
<method v="2" />
</configuration>
</component>

View File

@ -1,95 +0,0 @@
syntax = "v1"
info (
title: "redemption API"
desc: "API for redemption code management"
author: "Tension"
email: "tension@ppanel.com"
version: "0.0.1"
)
import "../types.api"
type (
CreateRedemptionCodeRequest {
TotalCount int64 `json:"total_count" validate:"required"`
SubscribePlan int64 `json:"subscribe_plan" validate:"required"`
UnitTime string `json:"unit_time" validate:"required,oneof=day month quarter half_year year"`
Quantity int64 `json:"quantity" validate:"required"`
BatchCount int64 `json:"batch_count" validate:"required,min=1"`
}
UpdateRedemptionCodeRequest {
Id int64 `json:"id" validate:"required"`
TotalCount int64 `json:"total_count,omitempty"`
SubscribePlan int64 `json:"subscribe_plan,omitempty"`
UnitTime string `json:"unit_time,omitempty" validate:"omitempty,oneof=day month quarter half_year year"`
Quantity int64 `json:"quantity,omitempty"`
Status int64 `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
}
ToggleRedemptionCodeStatusRequest {
Id int64 `json:"id" validate:"required"`
Status int64 `json:"status" validate:"oneof=0 1"`
}
DeleteRedemptionCodeRequest {
Id int64 `json:"id" validate:"required"`
}
BatchDeleteRedemptionCodeRequest {
Ids []int64 `json:"ids" validate:"required"`
}
GetRedemptionCodeListRequest {
Page int64 `form:"page" validate:"required"`
Size int64 `form:"size" validate:"required"`
SubscribePlan int64 `form:"subscribe_plan,omitempty"`
UnitTime string `form:"unit_time,omitempty"`
Code string `form:"code,omitempty"`
}
GetRedemptionCodeListResponse {
Total int64 `json:"total"`
List []RedemptionCode `json:"list"`
}
GetRedemptionRecordListRequest {
Page int64 `form:"page" validate:"required"`
Size int64 `form:"size" validate:"required"`
UserId int64 `form:"user_id,omitempty"`
CodeId int64 `form:"code_id,omitempty"`
}
GetRedemptionRecordListResponse {
Total int64 `json:"total"`
List []RedemptionRecord `json:"list"`
}
)
@server (
prefix: v1/admin/redemption
group: admin/redemption
middleware: AuthMiddleware
)
service ppanel {
@doc "Create redemption code"
@handler CreateRedemptionCode
post /code (CreateRedemptionCodeRequest)
@doc "Update redemption code"
@handler UpdateRedemptionCode
put /code (UpdateRedemptionCodeRequest)
@doc "Toggle redemption code status"
@handler ToggleRedemptionCodeStatus
put /code/status (ToggleRedemptionCodeStatusRequest)
@doc "Delete redemption code"
@handler DeleteRedemptionCode
delete /code (DeleteRedemptionCodeRequest)
@doc "Batch delete redemption code"
@handler BatchDeleteRedemptionCode
delete /code/batch (BatchDeleteRedemptionCodeRequest)
@doc "Get redemption code list"
@handler GetRedemptionCodeList
get /code/list (GetRedemptionCodeListRequest) returns (GetRedemptionCodeListResponse)
@doc "Get redemption record list"
@handler GetRedemptionRecordList
get /record/list (GetRedemptionRecordListRequest) returns (GetRedemptionRecordListResponse)
}

View File

@ -22,7 +22,6 @@ type (
Unscoped bool `form:"unscoped,omitempty"`
SubscribeId *int64 `form:"subscribe_id,omitempty"`
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
ShortCode string `form:"short_code,omitempty"`
}
// GetUserListResponse
GetUserListResponse {
@ -180,7 +179,7 @@ type (
Total int64 `json:"total"`
}
DeleteUserSubscribeRequest {
UserSubscribeId int64 `json:"user_subscribe_id,string"`
UserSubscribeId int64 `json:"user_subscribe_id"`
}
GetUserSubscribeByIdRequest {
Id int64 `form:"id" validate:"required"`

View File

@ -124,7 +124,6 @@ type (
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
}
)

View File

@ -1,32 +0,0 @@
syntax = "v1"
info (
title: "redemption API"
desc: "API for redemption"
author: "Tension"
email: "tension@ppanel.com"
version: "0.0.1"
)
import "../types.api"
type (
RedeemCodeRequest {
Code string `json:"code" validate:"required"`
}
RedeemCodeResponse {
Message string `json:"message"`
}
)
@server (
prefix: v1/public/redemption
group: public/redemption
jwt: JwtAuth
middleware: AuthMiddleware,DeviceMiddleware
)
service ppanel {
@doc "Redeem code"
@handler RedeemCode
post / (RedeemCodeRequest) returns (RedeemCodeResponse)
}

View File

@ -14,48 +14,40 @@ type (
QuerySubscribeListRequest {
Language string `form:"language"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"`
Download int64 `json:"download"`
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
}
UserSubscribeNodeInfo{
Id int64 `json:"id"`
Name string `json:"name"`
Uuid string `json:"uuid"`
Protocol string `json:"protocol"`
Protocols string `json:"protocols"`
Port uint16 `json:"port"`
Address string `json:"address"`
Tags []string `json:"tags"`
Country string `json:"country"`
City string `json:"city"`
Longitude string `json:"longitude"`
Latitude string `json:"latitude"`
LatitudeCenter string `json:"latitude_center"`
LongitudeCenter string `json:"longitude_center"`
CreatedAt int64 `json:"created_at"`
}
QueryUserSubscribeNodeListResponse {
List []UserSubscribeInfo `json:"list"`
}
UserSubscribeInfo {
Id int64 `json:"id"`
UserId int64 `json:"user_id"`
OrderId int64 `json:"order_id"`
SubscribeId int64 `json:"subscribe_id"`
StartTime int64 `json:"start_time"`
ExpireTime int64 `json:"expire_time"`
FinishedAt int64 `json:"finished_at"`
ResetTime int64 `json:"reset_time"`
Traffic int64 `json:"traffic"`
Download int64 `json:"download"`
Upload int64 `json:"upload"`
Token string `json:"token"`
Status uint8 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
IsTryOut bool `json:"is_try_out"`
Nodes []*UserSubscribeNodeInfo `json:"nodes"`
}
UserSubscribeNodeInfo {
Id int64 `json:"id"`
Name string `json:"name"`
Uuid string `json:"uuid"`
Protocol string `json:"protocol"`
Port uint16 `json:"port"`
Address string `json:"address"`
Tags []string `json:"tags"`
Country string `json:"country"`
City string `json:"city"`
CreatedAt int64 `json:"created_at"`
}
)
@server (
@ -68,8 +60,8 @@ service ppanel {
@handler QuerySubscribeList
get /list (QuerySubscribeListRequest) returns (QuerySubscribeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
@doc "Get user subscribe node info"
@handler QueryUserSubscribeNodeList
get /node/list returns (QueryUserSubscribeNodeListResponse)
}

View File

@ -66,7 +66,6 @@ type (
UnbindOAuthRequest {
Method string `json:"method"`
}
GetLoginLogRequest {
Page int `form:"page"`
Size int `form:"size"`
@ -95,21 +94,17 @@ type (
Email string `json:"email" validate:"required"`
Code string `json:"code" validate:"required"`
}
GetDeviceListResponse {
List []UserDevice `json:"list"`
Total int64 `json:"total"`
}
UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
}
GetDeviceListResponse {
List []UserDevice `json:"list"`
Total int64 `json:"total"`
}
UnbindDeviceRequest {
Id int64 `json:"id" validate:"required"`
}
UpdateUserSubscribeNoteRequest {
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required"`
Note string `json:"note" validate:"max=500"`
}
UpdateUserRulesRequest {
Rules []string `json:"rules" validate:"required"`
}
@ -135,23 +130,6 @@ type (
List []WithdrawalLog `json:"list"`
Total int64 `json:"total"`
}
GetDeviceOnlineStatsResponse {
WeeklyStats []WeeklyStat `json:"weekly_stats"`
ConnectionRecords ConnectionRecords `json:"connection_records"`
}
WeeklyStat {
Day int `json:"day"`
DayName string `json:"day_name"`
Hours float64 `json:"hours"`
}
ConnectionRecords {
CurrentContinuousDays int64 `json:"current_continuous_days"`
HistoryContinuousDays int64 `json:"history_continuous_days"`
LongestSingleConnection int64 `json:"longest_single_connection"`
}
)
@server (
@ -248,9 +226,9 @@ service ppanel {
@handler UpdateBindEmail
put /bind_email (UpdateBindEmailRequest)
@doc "Get Device List"
@handler GetDeviceList
get /devices returns (GetDeviceListResponse)
@doc "Get Device List"
@handler GetDeviceList
get /devices returns (GetDeviceListResponse)
@doc "Unbind Device"
@handler UnbindDevice
@ -271,24 +249,5 @@ service ppanel {
@doc "Query Withdrawal Log"
@handler QueryWithdrawalLog
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
@doc "Device Online Statistics"
@handler DeviceOnlineStatistics
get /device_online_statistics returns (GetDeviceOnlineStatsResponse)
@doc "Delete Current User Account"
@handler DeleteCurrentUserAccount
delete /current_user_account
}
@server(
prefix: v1/public/user
group: public/user/ws
middleware: AuthMiddleware
)
service ppanel {
@doc "Webosocket Device Connect"
@handler DeviceWsConnect
get /device_ws_connect
}

View File

@ -32,7 +32,6 @@ type (
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
DeletedAt int64 `json:"deleted_at,omitempty"`
IsDel bool `json:"is_del,omitempty"`
}
Follow {
Id int64 `json:"id"`
@ -151,7 +150,6 @@ type (
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
IpRegisterLimit int64 `json:"ip_register_limit"`
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
DeviceLimit int64 `json:"device_limit"`
}
VerifyConfig {
TurnstileSiteKey string `json:"turnstile_site_key"`
@ -206,7 +204,7 @@ type (
CurrencySymbol string `json:"currency_symbol"`
}
SubscribeDiscount {
Quantity int64 `json:"quantity"`
Quantity int64 `json:"quantity"`
Discount float64 `json:"discount"`
}
Subscribe {
@ -448,28 +446,6 @@ type (
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
RedemptionCode {
Id int64 `json:"id"`
Code string `json:"code"`
TotalCount int64 `json:"total_count"`
UsedCount int64 `json:"used_count"`
SubscribePlan int64 `json:"subscribe_plan"`
UnitTime string `json:"unit_time"`
Quantity int64 `json:"quantity"`
Status int64 `json:"status"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}
RedemptionRecord {
Id int64 `json:"id"`
RedemptionCodeId int64 `json:"redemption_code_id"`
UserId int64 `json:"user_id"`
SubscribeId int64 `json:"subscribe_id"`
UnitTime string `json:"unit_time"`
Quantity int64 `json:"quantity"`
RedeemedAt int64 `json:"redeemed_at"`
CreatedAt int64 `json:"created_at"`
}
Announcement {
Id int64 `json:"id"`
Title string `json:"title"`
@ -680,7 +656,7 @@ type (
// public announcement
QueryAnnouncementRequest {
Page int `form:"page"`
Size int `form:"size,default=15"`
Size int `form:"size"`
Pinned *bool `form:"pinned"`
Popup *bool `form:"popup"`
}
@ -697,7 +673,6 @@ type (
List []SubscribeGroup `json:"list"`
Total int64 `json:"total"`
}
GetUserSubscribeTrafficLogsRequest {
Page int `form:"page"`
Size int `form:"size"`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 MiB

View File

@ -1,73 +0,0 @@
package cmd
import (
"fmt"
"github.com/perfect-panel/server/pkg/updater"
"github.com/spf13/cobra"
)
var (
checkOnly bool
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "Check for updates and update PPanel to the latest version",
Long: `Check for available updates from GitHub releases and automatically
update the PPanel binary to the latest version.
Examples:
# Check for updates only
ppanel-server update --check
# Update to the latest version
ppanel-server update`,
Run: func(cmd *cobra.Command, args []string) {
u := updater.NewUpdater()
if checkOnly {
checkForUpdates(u)
return
}
performUpdate(u)
},
}
func init() {
updateCmd.Flags().BoolVarP(&checkOnly, "check", "c", false, "Check for updates without applying them")
}
func checkForUpdates(u *updater.Updater) {
fmt.Println("Checking for updates...")
release, hasUpdate, err := u.CheckForUpdates()
if err != nil {
fmt.Printf("Error checking for updates: %v\n", err)
return
}
if !hasUpdate {
fmt.Println("You are already running the latest version!")
return
}
fmt.Printf("\nNew version available!\n")
fmt.Printf("Current version: %s\n", u.CurrentVersion)
fmt.Printf("Latest version: %s\n", release.TagName)
fmt.Printf("\nRelease notes:\n%s\n", release.Body)
fmt.Printf("\nTo update, run: ppanel-server update\n")
}
func performUpdate(u *updater.Updater) {
fmt.Println("Starting update process...")
if err := u.Update(); err != nil {
fmt.Printf("Update failed: %v\n", err)
return
}
fmt.Println("\nUpdate completed successfully!")
fmt.Println("Please restart the application to use the new version.")
}

View File

@ -1,45 +0,0 @@
Host: # 服务监听地址
Port: 8080 # 服务监听端口, 默认: 8080
Debug: true # 是否开启调试模式, 默认: false
JwtAuth: # JWT认证配置
AccessSecret: CHANGE_ME_TO_A_RANDOM_SECRET # 访问令牌密钥, 请修改为随机字符串
AccessExpire: 604800 # 访问令牌过期时间,单位秒, 默认: 604800 (7天)
Logger: # 日志配置
FilePath: logs/ppanel.log # 日志文件路径
MaxSize: 50 # 日志文件最大大小, 单位MB
MaxBackup: 3 # 日志文件最大备份数
MaxAge: 30 # 日志文件最大保存时间,单位天
Compress: true # 是否压缩日志文件
Level: debug # 日志级别: debug, info, warn, error, panic, fatal
MySQL:
Addr: 127.0.0.1:3306 # MySQL地址
Username: root # MySQL用户名 (与创建的用户一致)
Password: password # MySQL密码 (换成之前生成的随机密码)
Dbname: ppanel # MySQL数据库名 (与脚本创建的数据库一致)
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
MaxIdleConns: 10
MaxOpenConns: 100
LogMode: debug
LogZap: true
SlowThreshold: 1000
Redis:
Host: 127.0.0.1:6379 # Redis地址格式host:port
Pass: # Redis密码如果没有设置密码可以留空
DB: 0 # Redis数据库编号默认0
PoolSize: 100 # 连接池大小最大连接数根据应用并发量调整建议小流量50-100中流量100-300大流量300-500
MinIdleConns: 10 # 最小空闲连接数保持一定数量的空闲连接以减少建立连接的开销建议为PoolSize的10%
MaxRetries: 3 # 最大重试次数网络抖动时自动重试建议2-3次
PoolTimeout: 4 # 连接池超时时间从连接池获取连接的最大等待时间建议3-5秒
IdleTimeout: 300 # 空闲连接超时时间自动回收长时间空闲的连接建议300-600秒5-10分钟
MaxConnAge: 0 # 连接最大生命周期定期重建连接避免长时间使用的问题0表示不限制建议7200秒2小时或0
DialTimeout: 5 # 连接超时时间建立新连接的超时时间建议5秒
ReadTimeout: 3 # 读操作超时时间建议2-3秒
WriteTimeout: 3 # 写操作超时时间建议2-3秒
Administrator:
Email: admin@ppanel.dev # 后台登录邮箱,请修改
Password: CHANGE_ME_TO_STRONG_PASSWORD # 后台登录密码,请修改为强密码

2
go.mod
View File

@ -27,7 +27,7 @@ require (
github.com/jinzhu/copier v0.4.0
github.com/klauspost/compress v1.17.7
github.com/nyaruka/phonenumbers v1.5.0
github.com/pkg/errors v0.9.1
github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.7.2
github.com/smartwalle/alipay/v3 v3.2.23
github.com/spf13/cast v1.7.0 // indirect

View File

@ -1,2 +1 @@
CREATE INDEX idx_timestamp ON traffic_log (timestamp);
ALTER TABLE traffic_log ADD INDEX idx_timestamp (timestamp);

View File

@ -1,78 +0,0 @@
-- Only add the columns to `servers` when they do not already exist
-- Add longitude
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'longitude'
);
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `longitude` VARCHAR(255) DEFAULT '''' COMMENT ''longitude'';',
'SELECT "Column `longitude` already exists"'
);
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add latitude
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'latitude'
);
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `latitude` VARCHAR(255) DEFAULT '''' COMMENT ''latitude'';',
'SELECT "Column `latitude` already exists"'
);
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add longitude_center
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'longitude_center'
);
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `longitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''longitude center'';',
'SELECT "Column `longitude_center` already exists"'
);
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Add latitude_center
SET @col_exists := (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'servers'
AND COLUMN_NAME = 'latitude_center'
);
SET @query := IF(
@col_exists = 0,
'ALTER TABLE `servers` ADD COLUMN `latitude_center` VARCHAR(255) DEFAULT '''' COMMENT ''latitude center'';',
'SELECT "Column `latitude_center` already exists"'
);
PREPARE stmt FROM @query;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@ -0,0 +1,27 @@
CREATE TABLE IF NOT EXISTS `server`
(
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Node Name',
`tags` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Tags',
`country` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Country',
`city` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'City',
`latitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'latitude',
`longitude` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'longitude',
`server_addr` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Server Address',
`relay_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'none' COMMENT 'Relay Mode',
`relay_node` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Relay Node',
`speed_limit` bigint NOT NULL DEFAULT '0' COMMENT 'Speed Limit',
`traffic_ratio` decimal(4, 2) NOT NULL DEFAULT '0.00' COMMENT 'Traffic Ratio',
`group_id` bigint DEFAULT NULL COMMENT 'Group ID',
`protocol` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT 'Protocol',
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'Config',
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Enabled',
`sort` bigint NOT NULL DEFAULT '0' COMMENT 'Sort',
`last_reported_at` datetime(3) DEFAULT NULL COMMENT 'Last Reported Time',
`created_at` datetime(3) DEFAULT NULL COMMENT 'Creation Time',
`updated_at` datetime(3) DEFAULT NULL COMMENT 'Update Time',
PRIMARY KEY (`id`),
KEY `idx_group_id` (`group_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4
COLLATE = utf8mb4_general_ci;

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS `server`;

View File

@ -1,5 +0,0 @@
-- Drop redemption_record table
DROP TABLE IF EXISTS `redemption_record`;
-- Drop redemption_code table
DROP TABLE IF EXISTS `redemption_code`;

View File

@ -1,31 +0,0 @@
-- Create redemption_code table
CREATE TABLE IF NOT EXISTS `redemption_code` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`code` VARCHAR(255) NOT NULL COMMENT 'Redemption Code',
`total_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Total Redemption Count',
`used_count` BIGINT NOT NULL DEFAULT 0 COMMENT 'Used Redemption Count',
`subscribe_plan` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Plan',
`unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time: day, month, quarter, half_year, year',
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity',
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
`updated_at` DATETIME NOT NULL COMMENT 'Update Time',
`deleted_at` DATETIME DEFAULT NULL COMMENT 'Deletion Time',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Code Table';
-- Create redemption_record table
CREATE TABLE IF NOT EXISTS `redemption_record` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Primary Key',
`redemption_code_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Redemption Code Id',
`user_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'User Id',
`subscribe_id` BIGINT NOT NULL DEFAULT 0 COMMENT 'Subscribe Id',
`unit_time` VARCHAR(50) NOT NULL DEFAULT 'month' COMMENT 'Unit Time',
`quantity` BIGINT NOT NULL DEFAULT 1 COMMENT 'Quantity',
`redeemed_at` DATETIME NOT NULL COMMENT 'Redeemed Time',
`created_at` DATETIME NOT NULL COMMENT 'Creation Time',
PRIMARY KEY (`id`),
KEY `idx_redemption_code_id` (`redemption_code_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Redemption Record Table';

View File

@ -1,2 +0,0 @@
-- Remove status column from redemption_code table
ALTER TABLE `redemption_code` DROP COLUMN `status`;

View File

@ -1,2 +0,0 @@
-- Add status column to redemption_code table
ALTER TABLE `redemption_code` ADD COLUMN `status` TINYINT NOT NULL DEFAULT 1 COMMENT 'Status: 1=enabled, 0=disabled' AFTER `quantity`;

View File

@ -1,2 +0,0 @@
-- Remove device limit configuration from system table
DELETE FROM `system` WHERE `category` = 'register' AND `key` = 'DeviceLimit';

View File

@ -1,3 +0,0 @@
-- Add device limit configuration to system table
INSERT IGNORE INTO `system` (`id`, `category`, `key`, `value`, `type`, `desc`, `created_at`, `updated_at`)
VALUES (43, 'register', 'DeviceLimit', '5', 'int', 'Device binding limit', NOW(), NOW());

View File

@ -1,2 +0,0 @@
-- Remove short_code column from user_device table
ALTER TABLE `user_device` DROP COLUMN `short_code`;

View File

@ -1,2 +0,0 @@
-- Add short_code column to user_device table
ALTER TABLE `user_device` ADD COLUMN `short_code` VARCHAR(255) DEFAULT '' COMMENT 'Short Code' AFTER `identifier`;

View File

@ -1,2 +0,0 @@
-- Remove index on refer_code column
ALTER TABLE `user` DROP INDEX `idx_refer_code`;

View File

@ -1,2 +0,0 @@
-- Add index on refer_code column for faster lookup
ALTER TABLE `user` ADD INDEX `idx_refer_code` (`refer_code`);

View File

@ -39,9 +39,6 @@ const VerifyCodeConfigKey = "system:verify_code_config"
// SessionIdKey cache session key
const SessionIdKey = "auth:session_id"
// DeviceCacheKeyKey cache session key
const DeviceCacheKeyKey = "auth:device_identifier"
// GlobalConfigKey Global Config Key
const GlobalConfigKey = "system:global_config"
@ -62,5 +59,3 @@ const SendIntervalKeyPrefix = "send:interval:"
// SendCountLimitKeyPrefix Send Count Limit Key Prefix eg. send:limit:register:email:xxx@ppanel.dev
const SendCountLimitKeyPrefix = "send:limit:"
const RegisterIpKeyPrefix = "register:ip:"

View File

@ -37,18 +37,9 @@ type Config struct {
}
type RedisConfig struct {
Host string `yaml:"Host" default:"localhost:6379"`
Pass string `yaml:"Pass" default:""`
DB int `yaml:"DB" default:"0"`
PoolSize int `yaml:"PoolSize" default:"100"` // 连接池大小(最大连接数)
MinIdleConns int `yaml:"MinIdleConns" default:"10"` // 最小空闲连接数
MaxRetries int `yaml:"MaxRetries" default:"3"` // 最大重试次数
PoolTimeout int `yaml:"PoolTimeout" default:"4"` // 连接池超时时间(秒)
IdleTimeout int `yaml:"IdleTimeout" default:"300"` // 空闲连接超时时间(秒)
MaxConnAge int `yaml:"MaxConnAge" default:"0"` // 连接最大生命周期0表示不限制
DialTimeout int `yaml:"DialTimeout" default:"5"` // 连接超时时间(秒)
ReadTimeout int `yaml:"ReadTimeout" default:"3"` // 读超时时间(秒)
WriteTimeout int `yaml:"WriteTimeout" default:"3"` // 写超时时间(秒)
Host string `yaml:"Host" default:"localhost:6379"`
Pass string `yaml:"Pass" default:""`
DB int `yaml:"DB" default:"0"`
}
type JwtAuth struct {
@ -82,7 +73,6 @@ type RegisterConfig struct {
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
DeviceLimit int64 `yaml:"DeviceLimit" default:"5"`
}
type EmailConfig struct {

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Batch delete redemption code
func BatchDeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.BatchDeleteRedemptionCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewBatchDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx)
err := l.BatchDeleteRedemptionCode(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Create redemption code
func CreateRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.CreateRedemptionCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewCreateRedemptionCodeLogic(c.Request.Context(), svcCtx)
err := l.CreateRedemptionCode(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Delete redemption code
func DeleteRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.DeleteRedemptionCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewDeleteRedemptionCodeLogic(c.Request.Context(), svcCtx)
err := l.DeleteRedemptionCode(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get redemption code list
func GetRedemptionCodeListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetRedemptionCodeListRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewGetRedemptionCodeListLogic(c.Request.Context(), svcCtx)
resp, err := l.GetRedemptionCodeList(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get redemption record list
func GetRedemptionRecordListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetRedemptionRecordListRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewGetRedemptionRecordListLogic(c.Request.Context(), svcCtx)
resp, err := l.GetRedemptionRecordList(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Toggle redemption code status
func ToggleRedemptionCodeStatusHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.ToggleRedemptionCodeStatusRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewToggleRedemptionCodeStatusLogic(c.Request.Context(), svcCtx)
err := l.ToggleRedemptionCodeStatus(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/admin/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Update redemption code
func UpdateRedemptionCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.UpdateRedemptionCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewUpdateRedemptionCodeLogic(c.Request.Context(), svcCtx)
err := l.UpdateRedemptionCode(&req)
result.HttpResult(c, nil, err)
}
}

View File

@ -1,26 +0,0 @@
package redemption
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Redeem code
func RedeemCodeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.RedeemCodeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := redemption.NewRedeemCodeLogic(c.Request.Context(), svcCtx)
resp, err := l.RedeemCode(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -1,18 +0,0 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Delete Current User Account
func DeleteCurrentUserAccountHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := user.NewDeleteCurrentUserAccountLogic(c.Request.Context(), svcCtx)
err := l.DeleteCurrentUserAccount()
result.HttpResult(c, nil, err)
}
}

View File

@ -1,18 +0,0 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
// Device Online Statistics
func DeviceOnlineStatisticsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := user.NewDeviceOnlineStatisticsLogic(c.Request.Context(), svcCtx)
resp, err := l.DeviceOnlineStatistics()
result.HttpResult(c, resp, err)
}
}

View File

@ -1,29 +0,0 @@
package ws
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
wslogic "github.com/perfect-panel/server/internal/logic/public/user/ws"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/result"
)
var upGrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有来源,生产环境中应该根据需求限制
},
}
// Webosocket Device Connect
func DeviceWsConnectHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
l := wslogic.NewDeviceWsConnectLogic(c.Request.Context(), svcCtx)
err := l.DeviceWsConnect(c)
result.HttpResult(c, nil, err)
}
}

View File

@ -16,7 +16,6 @@ import (
adminMarketing "github.com/perfect-panel/server/internal/handler/admin/marketing"
adminOrder "github.com/perfect-panel/server/internal/handler/admin/order"
adminPayment "github.com/perfect-panel/server/internal/handler/admin/payment"
adminRedemption "github.com/perfect-panel/server/internal/handler/admin/redemption"
adminServer "github.com/perfect-panel/server/internal/handler/admin/server"
adminSubscribe "github.com/perfect-panel/server/internal/handler/admin/subscribe"
adminSystem "github.com/perfect-panel/server/internal/handler/admin/system"
@ -31,11 +30,9 @@ import (
publicOrder "github.com/perfect-panel/server/internal/handler/public/order"
publicPayment "github.com/perfect-panel/server/internal/handler/public/payment"
publicPortal "github.com/perfect-panel/server/internal/handler/public/portal"
publicRedemption "github.com/perfect-panel/server/internal/handler/public/redemption"
publicSubscribe "github.com/perfect-panel/server/internal/handler/public/subscribe"
publicTicket "github.com/perfect-panel/server/internal/handler/public/ticket"
publicUser "github.com/perfect-panel/server/internal/handler/public/user"
publicUserWs "github.com/perfect-panel/server/internal/handler/public/user/ws"
server "github.com/perfect-panel/server/internal/handler/server"
"github.com/perfect-panel/server/internal/middleware"
"github.com/perfect-panel/server/internal/svc"
@ -301,32 +298,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
adminPaymentGroupRouter.GET("/platform", adminPayment.GetPaymentPlatformHandler(serverCtx))
}
adminRedemptionGroupRouter := router.Group("/v1/admin/redemption")
adminRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
{
// Create redemption code
adminRedemptionGroupRouter.POST("/code", adminRedemption.CreateRedemptionCodeHandler(serverCtx))
// Update redemption code
adminRedemptionGroupRouter.PUT("/code", adminRedemption.UpdateRedemptionCodeHandler(serverCtx))
// Delete redemption code
adminRedemptionGroupRouter.DELETE("/code", adminRedemption.DeleteRedemptionCodeHandler(serverCtx))
// Batch delete redemption code
adminRedemptionGroupRouter.DELETE("/code/batch", adminRedemption.BatchDeleteRedemptionCodeHandler(serverCtx))
// Get redemption code list
adminRedemptionGroupRouter.GET("/code/list", adminRedemption.GetRedemptionCodeListHandler(serverCtx))
// Toggle redemption code status
adminRedemptionGroupRouter.PUT("/code/status", adminRedemption.ToggleRedemptionCodeStatusHandler(serverCtx))
// Get redemption record list
adminRedemptionGroupRouter.GET("/record/list", adminRedemption.GetRedemptionRecordListHandler(serverCtx))
}
adminServerGroupRouter := router.Group("/v1/admin/server")
adminServerGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
@ -777,14 +748,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicPortalGroupRouter.GET("/subscribe", publicPortal.GetSubscriptionHandler(serverCtx))
}
publicRedemptionGroupRouter := router.Group("/v1/public/redemption")
publicRedemptionGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
{
// Redeem code
publicRedemptionGroupRouter.POST("/", publicRedemption.RedeemCodeHandler(serverCtx))
}
publicSubscribeGroupRouter := router.Group("/v1/public/subscribe")
publicSubscribeGroupRouter.Use(middleware.AuthMiddleware(serverCtx), middleware.DeviceMiddleware(serverCtx))
@ -850,12 +813,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Commission Withdraw
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
// Delete Current User Account
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
// Device Online Statistics
publicUserGroupRouter.GET("/device_online_statistics", publicUser.DeviceOnlineStatisticsHandler(serverCtx))
// Get Device List
publicUserGroupRouter.GET("/devices", publicUser.GetDeviceListHandler(serverCtx))
@ -911,14 +868,6 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
publicUserGroupRouter.GET("/withdrawal_log", publicUser.QueryWithdrawalLogHandler(serverCtx))
}
publicUserWsGroupRouter := router.Group("/v1/public/user")
publicUserWsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
{
// Webosocket Device Connect
publicUserWsGroupRouter.GET("/device_ws_connect", publicUserWs.DeviceWsConnectHandler(serverCtx))
}
serverGroupRouter := router.Group("/v1/server")
serverGroupRouter.Use(middleware.ServerMiddleware(serverCtx))
@ -939,10 +888,10 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
serverGroupRouter.GET("/user", server.GetServerUserListHandler(serverCtx))
}
serverV2GroupRouter := router.Group("/v2/server")
serverGroupRouter := router.Group("/v2/server")
{
// Get Server Protocol Config
serverV2GroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
serverGroupRouter.GET("/:server_id", server.QueryServerProtocolConfigHandler(serverCtx))
}
}

View File

@ -1,36 +0,0 @@
package redemption
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type BatchDeleteRedemptionCodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Batch delete redemption code
func NewBatchDeleteRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchDeleteRedemptionCodeLogic {
return &BatchDeleteRedemptionCodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *BatchDeleteRedemptionCodeLogic) BatchDeleteRedemptionCode(req *types.BatchDeleteRedemptionCodeRequest) error {
err := l.svcCtx.RedemptionCodeModel.BatchDelete(l.ctx, req.Ids)
if err != nil {
l.Errorw("[BatchDeleteRedemptionCode] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "batch delete redemption code error: %v", err.Error())
}
return nil
}

View File

@ -1,120 +0,0 @@
package redemption
import (
"context"
"crypto/rand"
"math/big"
"github.com/perfect-panel/server/internal/model/redemption"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type CreateRedemptionCodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Create redemption code
func NewCreateRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateRedemptionCodeLogic {
return &CreateRedemptionCodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// generateUniqueCode generates a unique redemption code
func (l *CreateRedemptionCodeLogic) generateUniqueCode() (string, error) {
const charset = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" // Removed confusing characters like I, O, 0, 1
const codeLength = 16
maxRetries := 10
for i := 0; i < maxRetries; i++ {
code := make([]byte, codeLength)
for j := 0; j < codeLength; j++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
if err != nil {
return "", err
}
code[j] = charset[num.Int64()]
}
codeStr := string(code)
// Check if code already exists
_, err := l.svcCtx.RedemptionCodeModel.FindOneByCode(l.ctx, codeStr)
if errors.Is(err, gorm.ErrRecordNotFound) {
return codeStr, nil
} else if err != nil {
return "", err
}
// Code exists, try again
}
return "", errors.New("failed to generate unique code after maximum retries")
}
func (l *CreateRedemptionCodeLogic) CreateRedemptionCode(req *types.CreateRedemptionCodeRequest) error {
// Check if subscribe plan is valid
if req.SubscribePlan == 0 {
l.Errorw("[CreateRedemptionCode] Subscribe plan cannot be empty")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "subscribe plan cannot be empty")
}
// Verify subscribe plan exists
_, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribePlan)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("[CreateRedemptionCode] Subscribe plan not found", logger.Field("subscribe_plan", req.SubscribePlan))
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "subscribe plan not found")
}
l.Errorw("[CreateRedemptionCode] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe plan error: %v", err.Error())
}
// Validate batch count
if req.BatchCount < 1 {
l.Errorw("[CreateRedemptionCode] Batch count must be at least 1")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "batch count must be at least 1")
}
// Generate redemption codes in batch
var createdCodes []string
for i := int64(0); i < req.BatchCount; i++ {
code, err := l.generateUniqueCode()
if err != nil {
l.Errorw("[CreateRedemptionCode] Failed to generate unique code", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "generate unique code error: %v", err.Error())
}
redemptionCode := &redemption.RedemptionCode{
Code: code,
TotalCount: req.TotalCount,
UsedCount: 0,
SubscribePlan: req.SubscribePlan,
UnitTime: req.UnitTime,
Quantity: req.Quantity,
Status: 1, // Default to enabled
}
err = l.svcCtx.RedemptionCodeModel.Insert(l.ctx, redemptionCode)
if err != nil {
l.Errorw("[CreateRedemptionCode] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create redemption code error: %v", err.Error())
}
createdCodes = append(createdCodes, code)
}
l.Infow("[CreateRedemptionCode] Successfully created redemption codes",
logger.Field("count", len(createdCodes)),
logger.Field("codes", createdCodes))
return nil
}

View File

@ -1,36 +0,0 @@
package redemption
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type DeleteRedemptionCodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Delete redemption code
func NewDeleteRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteRedemptionCodeLogic {
return &DeleteRedemptionCodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteRedemptionCodeLogic) DeleteRedemptionCode(req *types.DeleteRedemptionCodeRequest) error {
err := l.svcCtx.RedemptionCodeModel.Delete(l.ctx, req.Id)
if err != nil {
l.Errorw("[DeleteRedemptionCode] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete redemption code error: %v", err.Error())
}
return nil
}

View File

@ -1,62 +0,0 @@
package redemption
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetRedemptionCodeListLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get redemption code list
func NewGetRedemptionCodeListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRedemptionCodeListLogic {
return &GetRedemptionCodeListLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetRedemptionCodeListLogic) GetRedemptionCodeList(req *types.GetRedemptionCodeListRequest) (resp *types.GetRedemptionCodeListResponse, err error) {
total, list, err := l.svcCtx.RedemptionCodeModel.QueryRedemptionCodeListByPage(
l.ctx,
int(req.Page),
int(req.Size),
req.SubscribePlan,
req.UnitTime,
req.Code,
)
if err != nil {
l.Errorw("[GetRedemptionCodeList] Database Error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get redemption code list error: %v", err.Error())
}
var redemptionCodes []types.RedemptionCode
for _, item := range list {
redemptionCodes = append(redemptionCodes, types.RedemptionCode{
Id: item.Id,
Code: item.Code,
TotalCount: item.TotalCount,
UsedCount: item.UsedCount,
SubscribePlan: item.SubscribePlan,
UnitTime: item.UnitTime,
Quantity: item.Quantity,
Status: item.Status,
CreatedAt: item.CreatedAt.Unix(),
UpdatedAt: item.UpdatedAt.Unix(),
})
}
return &types.GetRedemptionCodeListResponse{
Total: total,
List: redemptionCodes,
}, nil
}

View File

@ -1,59 +0,0 @@
package redemption
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetRedemptionRecordListLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get redemption record list
func NewGetRedemptionRecordListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRedemptionRecordListLogic {
return &GetRedemptionRecordListLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetRedemptionRecordListLogic) GetRedemptionRecordList(req *types.GetRedemptionRecordListRequest) (resp *types.GetRedemptionRecordListResponse, err error) {
total, list, err := l.svcCtx.RedemptionRecordModel.QueryRedemptionRecordListByPage(
l.ctx,
int(req.Page),
int(req.Size),
req.UserId,
req.CodeId,
)
if err != nil {
l.Errorw("[GetRedemptionRecordList] Database Error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get redemption record list error: %v", err.Error())
}
var redemptionRecords []types.RedemptionRecord
for _, item := range list {
redemptionRecords = append(redemptionRecords, types.RedemptionRecord{
Id: item.Id,
RedemptionCodeId: item.RedemptionCodeId,
UserId: item.UserId,
SubscribeId: item.SubscribeId,
UnitTime: item.UnitTime,
Quantity: item.Quantity,
RedeemedAt: item.RedeemedAt.Unix(),
CreatedAt: item.CreatedAt.Unix(),
})
}
return &types.GetRedemptionRecordListResponse{
Total: total,
List: redemptionRecords,
}, nil
}

View File

@ -1,55 +0,0 @@
package redemption
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type ToggleRedemptionCodeStatusLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Toggle redemption code status
func NewToggleRedemptionCodeStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ToggleRedemptionCodeStatusLogic {
return &ToggleRedemptionCodeStatusLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ToggleRedemptionCodeStatusLogic) ToggleRedemptionCodeStatus(req *types.ToggleRedemptionCodeStatusRequest) error {
// Find redemption code
codeInfo, err := l.svcCtx.RedemptionCodeModel.FindOne(l.ctx, req.Id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("[ToggleRedemptionCodeStatus] Redemption code not found", logger.Field("id", req.Id))
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code not found")
}
l.Errorw("[ToggleRedemptionCodeStatus] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error())
}
// Update status
codeInfo.Status = req.Status
err = l.svcCtx.RedemptionCodeModel.Update(l.ctx, codeInfo)
if err != nil {
l.Errorw("[ToggleRedemptionCodeStatus] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update redemption code status error: %v", err.Error())
}
l.Infow("[ToggleRedemptionCodeStatus] Successfully toggled redemption code status",
logger.Field("id", req.Id),
logger.Field("status", req.Status))
return nil
}

View File

@ -1,65 +0,0 @@
package redemption
import (
"context"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type UpdateRedemptionCodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Update redemption code
func NewUpdateRedemptionCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateRedemptionCodeLogic {
return &UpdateRedemptionCodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *UpdateRedemptionCodeLogic) UpdateRedemptionCode(req *types.UpdateRedemptionCodeRequest) error {
redemptionCode, err := l.svcCtx.RedemptionCodeModel.FindOne(l.ctx, req.Id)
if err != nil {
l.Errorw("[UpdateRedemptionCode] Find Redemption Code Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error())
}
// Code is not allowed to be modified
if req.TotalCount != 0 {
// Total count cannot be less than used count
if req.TotalCount < redemptionCode.UsedCount {
l.Errorw("[UpdateRedemptionCode] Total count cannot be less than used count",
logger.Field("total_count", req.TotalCount),
logger.Field("used_count", redemptionCode.UsedCount))
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams),
"total count cannot be less than used count: total_count=%d, used_count=%d",
req.TotalCount, redemptionCode.UsedCount)
}
redemptionCode.TotalCount = req.TotalCount
}
if req.SubscribePlan != 0 {
redemptionCode.SubscribePlan = req.SubscribePlan
}
if req.UnitTime != "" {
redemptionCode.UnitTime = req.UnitTime
}
if req.Quantity != 0 {
redemptionCode.Quantity = req.Quantity
}
err = l.svcCtx.RedemptionCodeModel.Update(l.ctx, redemptionCode)
if err != nil {
l.Errorw("[UpdateRedemptionCode] Database Error", logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update redemption code error: %v", err.Error())
}
return nil
}

View File

@ -99,10 +99,6 @@ func (l *CreateServerLogic) CreateServer(req *types.CreateServerRequest) error {
} else {
data.City = result.City
data.Country = result.Country
data.Latitude = result.Latitude
data.Longitude = result.Longitude
data.LatitudeCenter = result.LatitudeCenter
data.LongitudeCenter = result.LongitudeCenter
}
}
err = l.svcCtx.NodeModel.InsertServer(l.ctx, &data)

View File

@ -39,7 +39,7 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
data.Country = req.Country
data.City = req.City
// only update address when it's different
if req.Address != data.Address || (data.Country == "" || req.Country == "") {
if req.Address != data.Address {
// query server ip location
result, err := ip.GetRegionByIp(req.Address)
if err != nil {
@ -47,10 +47,6 @@ func (l *UpdateServerLogic) UpdateServer(req *types.UpdateServerRequest) error {
} else {
data.City = result.City
data.Country = result.Country
data.Latitude = result.Latitude
data.Longitude = result.Longitude
data.LatitudeCenter = result.LatitudeCenter
data.LongitudeCenter = result.LongitudeCenter
}
// update address
data.Address = req.Address

View File

@ -2,12 +2,14 @@ package system
import (
"context"
"strings"
"os"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetModuleConfigLogic struct {
@ -26,14 +28,14 @@ func NewGetModuleConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext) *G
}
func (l *GetModuleConfigLogic) GetModuleConfig() (resp *types.ModuleConfig, err error) {
//value, exists := os.LookupEnv("SECRET_KEY")
//if !exists {
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
//}
value, exists := os.LookupEnv("SECRET_KEY")
if !exists {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), " SECRET_KEY not set in environment variables")
}
return &types.ModuleConfig{
//Secret: value,
Secret: value,
ServiceName: constant.ServiceName,
ServiceVersion: strings.ReplaceAll(constant.Version, "v", ""),
ServiceVersion: constant.Version,
}, nil
}

View File

@ -48,9 +48,5 @@ func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubs
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId))
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
}

View File

@ -33,7 +33,6 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
Unscoped: req.Unscoped,
SubscribeId: req.SubscribeId,
UserSubscribeId: req.UserSubscribeId,
ShortCode: req.ShortCode,
Order: "DESC",
})
if err != nil {

View File

@ -69,10 +69,5 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs
l.Errorw("failed to clear subscribe cache", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
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
}

View File

@ -2,7 +2,6 @@ package auth
import (
"context"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
@ -89,36 +88,6 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
logger.Field("user_id", userId),
)
// Check device limit
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
if deviceLimit > 0 {
// Count current user's devices
var deviceCount int64
if err := l.svcCtx.DB.Model(&user.Device{}).Where("user_id = ?", userId).Count(&deviceCount).Error; err != nil {
l.Errorw("failed to count user devices",
logger.Field("user_id", userId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count devices failed: %v", err.Error())
}
// Check if limit reached
if deviceCount >= deviceLimit {
l.Errorw("device limit reached",
logger.Field("user_id", userId),
logger.Field("device_count", deviceCount),
logger.Field("device_limit", deviceLimit),
)
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device limit reached: maximum %d devices allowed", deviceLimit)
}
l.Infow("device limit check passed",
logger.Field("user_id", userId),
logger.Field("device_count", deviceCount),
logger.Field("device_limit", deviceLimit),
)
}
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
// Create device auth method
authMethod := &user.AuthMethods{
@ -138,9 +107,8 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
// Create device record
deviceInfo := &user.Device{
Ip: ip,
UserId: userId,
Ip: ip,
UserId: userId,
UserAgent: userAgent,
Identifier: identifier,
Enabled: true,
@ -178,87 +146,10 @@ func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string,
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
oldUserId := deviceInfo.UserId
// Check device limit for new user
deviceLimit := l.svcCtx.Config.Register.DeviceLimit
if deviceLimit > 0 {
// Count new user's current devices (excluding the one being rebound)
var deviceCount int64
if err := l.svcCtx.DB.Model(&user.Device{}).Where("user_id = ? AND id != ?", newUserId, deviceInfo.Id).Count(&deviceCount).Error; err != nil {
l.Errorw("failed to count new user devices",
logger.Field("user_id", newUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count devices failed: %v", err.Error())
}
// Check if limit reached
if deviceCount >= deviceLimit {
l.Errorw("device limit reached for new user",
logger.Field("user_id", newUserId),
logger.Field("device_count", deviceCount),
logger.Field("device_limit", deviceLimit),
)
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device limit reached: maximum %d devices allowed", deviceLimit)
}
l.Infow("device limit check passed for rebinding",
logger.Field("user_id", newUserId),
logger.Field("device_count", deviceCount),
logger.Field("device_limit", deviceLimit),
)
}
var users []*user.User
err := l.svcCtx.DB.Where("id in (?)", []int64{oldUserId, newUserId}).Find(&users).Error
if err != nil {
l.Errorw("failed to query users for rebinding",
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query users failed: %v", err)
}
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
//检查旧设备是否存在认证方式
var authMethod user.AuthMethods
err := tx.Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).Find(&authMethod).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("failed to query device auth method",
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device auth method failed: %v", err)
}
//未找到设备认证方式信息,创建新的设备认证方式
if err != nil {
authMethod = user.AuthMethods{
UserId: newUserId,
AuthType: "device",
AuthIdentifier: deviceInfo.Identifier,
Verified: true,
}
logger.Infof("create auth method: %v", authMethod)
if err := tx.Create(&authMethod).Error; err != nil {
l.Errorw("failed to create device auth method", logger.Field("new_user_id", newUserId),
logger.Field("error", err.Error()))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
}
} else {
//更新设备认证方式的用户ID为新用户ID
authMethod.UserId = newUserId
if err := tx.Save(&authMethod).Error; err != nil {
l.Errorw("failed to update device auth method",
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
}
}
//检查旧用户是否还有其他认证方式
var count int64
if err := tx.Model(&user.AuthMethods{}).Where("user_id = ?", oldUserId).Count(&count).Error; err != nil {
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
// Check if old user has other auth methods besides device
var authMethods []user.AuthMethods
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
l.Errorw("failed to query auth methods for old user",
logger.Field("old_user_id", oldUserId),
logger.Field("error", err.Error()),
@ -266,113 +157,60 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
}
//如果没有其他认证方式,禁用旧用户账号
if count < 1 {
//检查设备下是否有套餐,有套餐。就检查即将绑定过去的所有账户是否有套餐,如果有,那么检查两个套餐是否一致。如果一致就将即将删除的用户套餐,时间叠加到我绑定过去的用户套餐上面(如果套餐已过期就忽略)。新绑定设备的账户上套餐不一致或者不存在直接将套餐换绑即可
var oldUserSubscribes []user.Subscribe
err = tx.Where("user_id = ? AND status IN ?", oldUserId, []int64{0, 1}).Find(&oldUserSubscribes).Error
if err != nil {
l.Errorw("failed to query old user subscribes",
logger.Field("old_user_id", oldUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query old user subscribes failed: %v", err)
// Count non-device auth methods
nonDeviceAuthCount := 0
for _, auth := range authMethods {
if auth.AuthType != "device" {
nonDeviceAuthCount++
}
}
if len(oldUserSubscribes) > 0 {
l.Infow("processing old user subscribes",
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),
logger.Field("subscribe_count", len(oldUserSubscribes)),
)
for _, oldSub := range oldUserSubscribes {
// 检查新用户是否有相同套餐ID的订阅
var newUserSub user.Subscribe
err = tx.Where("user_id = ? AND subscribe_id = ? AND status IN ?", newUserId, oldSub.SubscribeId, []int64{0, 1}).First(&newUserSub).Error
if err != nil {
// 新用户没有该套餐,直接换绑
oldSub.UserId = newUserId
if err := tx.Save(&oldSub).Error; err != nil {
l.Errorw("failed to rebind subscribe to new user",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "rebind subscribe failed: %v", err)
}
l.Infow("rebind subscribe to new user",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("new_user_id", newUserId),
)
} else {
// 新用户已有该套餐,检查旧套餐是否过期
now := time.Now()
if oldSub.ExpireTime.After(now) {
// 旧套餐未过期,叠加剩余时间
remainingDuration := oldSub.ExpireTime.Sub(now)
if newUserSub.ExpireTime.After(now) {
// 新套餐未过期,叠加时间
newUserSub.ExpireTime = newUserSub.ExpireTime.Add(remainingDuration)
} else {
newUserSub.ExpireTime = time.Now().Add(remainingDuration)
}
if err := tx.Save(&newUserSub).Error; err != nil {
l.Errorw("failed to update subscribe expire time",
logger.Field("subscribe_id", newUserSub.Id),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscribe expire time failed: %v", err)
}
l.Infow("merged subscribe time",
logger.Field("subscribe_id", newUserSub.Id),
logger.Field("new_expire_time", newUserSub.ExpireTime),
)
} else {
l.Infow("old subscribe expired, skip merge",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("expire_time", oldSub.ExpireTime),
)
}
// 删除旧用户的套餐
if err := tx.Delete(&oldSub).Error; err != nil {
l.Errorw("failed to delete old subscribe",
logger.Field("subscribe_id", oldSub.Id),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete old subscribe failed: %v", err)
}
}
}
}
if err := tx.Model(&user.User{}).Where("id = ?", oldUserId).Delete(&user.User{}).Error; err != nil {
// Only disable old user if they have no other auth methods
if nonDeviceAuthCount == 0 {
falseVal := false
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
l.Errorw("failed to disable old user",
logger.Field("old_user_id", oldUserId),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
}
l.Infow("disabled old user (no other auth methods)",
logger.Field("old_user_id", oldUserId),
)
} else {
l.Infow("old user has other auth methods, not disabling",
logger.Field("old_user_id", oldUserId),
logger.Field("non_device_auth_count", nonDeviceAuthCount),
)
}
l.Infow("disabled old user (no other auth methods)",
logger.Field("old_user_id", oldUserId),
)
// Update device auth method to new user
if err := db.Model(&user.AuthMethods{}).
Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).
Update("user_id", newUserId).Error; err != nil {
l.Errorw("failed to update device auth method",
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
}
// 更新设备绑定的用户id
// Update device record
deviceInfo.UserId = newUserId
deviceInfo.Ip = ip
deviceInfo.UserAgent = userAgent
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",
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("error", err.Error()),
)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
}
return nil
})
@ -386,15 +224,6 @@ func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, use
return err
}
err = l.svcCtx.UserModel.ClearUserCache(l.ctx, users...)
if err != nil {
l.Errorw("failed to clear user cache after rebinding",
logger.Field("old_user_id", oldUserId),
logger.Field("new_user_id", newUserId),
logger.Field("error", err.Error()),
)
}
l.Infow("device rebound successfully",
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("old_user_id", oldUserId),

View File

@ -71,9 +71,6 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "device", req.Identifier) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
}
// Device not found, create new user and device
userInfo, err = l.registerUserAndDevice(req)
if err != nil {
@ -128,17 +125,6 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
}
// Store device id in redis
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
if err = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
l.Errorw("set device id error",
logger.Field("user_id", userInfo.Id),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set device id error: %v", err.Error())
}
loginStatus = true
return &types.LoginResponse{
Token: token,
@ -152,11 +138,9 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
)
var userInfo *user.User
var trialSubscribe *user.Subscribe
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
// Create new user
userInfo = &user.User{
Salt: "default",
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
}
if err := db.Create(userInfo).Error; err != nil {
@ -198,7 +182,6 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
UserId: userInfo.Id,
UserAgent: req.UserAgent,
Identifier: req.Identifier,
ShortCode: req.ShortCode,
Enabled: true,
Online: false,
}
@ -213,10 +196,8 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
// Activate trial if enabled
if l.svcCtx.Config.Register.EnableTrial {
var trialErr error
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, db)
if trialErr != nil {
return trialErr
if err := l.activeTrial(userInfo.Id, db); err != nil {
return err
}
}
@ -231,25 +212,6 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
return nil, err
}
// Clear cache after transaction success
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
// Clear user subscription cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
// Don't return error, just log it
}
// Clear subscription cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
// Don't return error, just log it
}
// Clear all server cache
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
l.Errorf("ClearServerAllCache error: %v", err.Error())
// Don't return error, just log it
}
}
l.Infow("device registration completed successfully",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
@ -282,7 +244,7 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
return userInfo, nil
}
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscribe, error) {
func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) error {
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
if err != nil {
l.Errorw("failed to find trial subscription template",
@ -290,7 +252,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscri
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
logger.Field("error", err.Error()),
)
return nil, err
return err
}
startTime := time.Now()
@ -317,7 +279,7 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscri
logger.Field("user_id", userId),
logger.Field("error", err.Error()),
)
return nil, err
return err
}
l.Infow("trial subscription activated successfully",
@ -327,5 +289,5 @@ func (l *DeviceLoginLogic) activeTrial(userId int64, db *gorm.DB) (*user.Subscri
logger.Field("traffic", sub.Traffic),
)
return userSub, nil
return nil
}

View File

@ -341,7 +341,6 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
}
var userInfo *user.User
var trialSubscribe *user.Subscribe
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
if email != "" {
l.Debugw("checking if email already exists",
@ -398,10 +397,8 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
logger.Field("request_id", requestID),
logger.Field("user_id", userInfo.Id),
)
var trialErr error
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, requestID)
if trialErr != nil {
return trialErr
if err := l.activeTrial(userInfo.Id, requestID); err != nil {
return err
}
}
@ -418,25 +415,6 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
return userInfo, err
}
// Clear cache after transaction success
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
// Clear user subscription cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
// Don't return error, just log it
}
// Clear subscription cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
// Don't return error, just log it
}
// Clear all server cache
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
l.Errorf("ClearServerAllCache error: %v", err.Error())
// Don't return error, just log it
}
}
l.Infow("user registration completed successfully",
logger.Field("request_id", requestID),
logger.Field("user_id", userInfo.Id),
@ -815,7 +793,7 @@ func (l *OAuthLoginGetTokenLogic) findOrRegisterUser(authType, openID, email, av
return userInfo, nil
}
func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*user.Subscribe, error) {
func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) error {
l.Debugw("fetching trial subscription template",
logger.Field("request_id", requestID),
logger.Field("user_id", uid),
@ -830,7 +808,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
logger.Field("trial_subscribe_id", l.svcCtx.Config.Register.TrialSubscribe),
logger.Field("error", err.Error()),
)
return nil, err
return err
}
startTime := time.Now()
@ -870,7 +848,7 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
logger.Field("user_id", uid),
logger.Field("error", err.Error()),
)
return nil, err
return err
}
l.Infow("trial subscription activated successfully",
@ -880,5 +858,5 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
logger.Field("expire_time", expireTime),
logger.Field("traffic", sub.Traffic),
)
return userSub, nil
return nil
}

View File

@ -1,65 +0,0 @@
package auth
import (
"context"
"fmt"
"time"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)
func registerIpLimit(svcCtx *svc.ServiceContext, ctx context.Context, registerIp, authType, account string) (isOk bool) {
if !svcCtx.Config.Register.EnableIpRegisterLimit {
return true
}
// Use a sorted set to track IP registrations with timestamp as score
// Key format: register:ip:{ip}
key := fmt.Sprintf("%s%s", config.RegisterIpKeyPrefix, registerIp)
now := time.Now().Unix()
expiration := int64(svcCtx.Config.Register.IpRegisterLimitDuration) * 60
// Clean up expired entries first (remove entries older than expiration duration)
expireTimestamp := now - expiration
removed, err := svcCtx.Redis.ZRemRangeByScore(ctx, key, "0", fmt.Sprintf("%d", expireTimestamp)).Result()
if err != nil {
zap.S().Errorf("[registerIpLimit] ZRemRangeByScore Err: %v", err)
return true
}
if removed > 0 {
zap.S().Debugf("[registerIpLimit] Cleaned %d expired entries for IP: %s", removed, registerIp)
}
// Get current count of registrations within the time window
count, err := svcCtx.Redis.ZCard(ctx, key).Result()
if err != nil {
zap.S().Errorf("[registerIpLimit] ZCard Err: %v", err)
return true
}
// Check if limit is reached
if count >= svcCtx.Config.Register.IpRegisterLimit {
zap.S().Warnf("[registerIpLimit] IP %s exceeded limit: %d/%d", registerIp, count, svcCtx.Config.Register.IpRegisterLimit)
return false
}
// Add new registration entry with current timestamp as score
member := fmt.Sprintf("%s:%s", authType, account)
if err := svcCtx.Redis.ZAdd(ctx, key, redis.Z{
Score: float64(now),
Member: member,
}).Err(); err != nil {
zap.S().Errorf("[registerIpLimit] ZAdd Err: %v", err)
return true
}
// Set expiration on the sorted set key
if err := svcCtx.Redis.Expire(ctx, key, time.Minute*time.Duration(svcCtx.Config.Register.IpRegisterLimitDuration)).Err(); err != nil {
zap.S().Errorf("[registerIpLimit] Expire Err: %v", err)
}
return true
}

View File

@ -121,8 +121,8 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
// Don't fail register if device binding fails, just log the error
}
}
if l.ctx.Value(constant.CtxLoginType) != nil {
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
if l.ctx.Value(constant.LoginType) != nil {
req.LoginType = l.ctx.Value(constant.LoginType).(string)
}
// Generate session id
sessionId := uuidx.NewUUID().String()
@ -133,8 +133,7 @@ func (l *ResetPasswordLogic) ResetPassword(req *types.ResetPasswordRequest) (res
l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId),
jwt.WithOption("identifier", req.Identifier),
jwt.WithOption("CtxLoginType", req.LoginType),
jwt.WithOption("LoginType", req.LoginType),
)
if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -10,6 +10,7 @@ import (
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
@ -47,22 +48,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
}
loginStatus := false
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())
}
var userInfo *user.User
// Record login status
defer func(svcCtx *svc.ServiceContext) {
if userInfo.Id != 0 {
@ -90,6 +76,22 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
}
}(l.svcCtx)
authMethodInfo, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "mobile", phoneNumber)
if err != nil {
if errors.As(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
}
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethodInfo.UserId)
if err != nil {
if errors.As(err, gorm.ErrRecordNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "user telephone not exist: %v", req.Telephone)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user info failed: %v", err.Error())
}
if req.Password == "" && req.TelephoneCode == "" {
return nil, xerr.NewErrCodeMsg(xerr.InvalidParams, "password and telephone code is empty")
}
@ -135,8 +137,8 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
}
}
if l.ctx.Value(constant.CtxLoginType) != nil {
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
if l.ctx.Value(constant.LoginType) != nil {
req.LoginType = l.ctx.Value(constant.LoginType).(string)
}
// Generate session id
@ -148,8 +150,7 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId),
jwt.WithOption("identifier", req.Identifier),
jwt.WithOption("CtxLoginType", req.LoginType),
jwt.WithOption("LoginType", req.LoginType),
)
if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -2,7 +2,6 @@ package auth
import (
"context"
"encoding/json"
"fmt"
"time"
@ -44,32 +43,19 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
}
if !l.svcCtx.Config.Mobile.Enable {
if l.svcCtx.Config.Mobile.Enable {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SmsNotEnabled), "sms login is not enabled")
}
// if the email verification is enabled, the verification code is required
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.ParseVerifyType(uint8(constant.Security)), phoneNumber)
l.Logger.Infof("TelephoneResetPassword cacheKey: %s, code: %s", cacheKey, code)
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeTelephoneCacheKey, constant.Security, phoneNumber)
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
if err != nil {
l.Errorw("Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
}
l.Logger.Infof("TelephoneResetPassword cacheKey: %s, code: %s,value : %s", cacheKey, code, value)
if value == "" {
l.Errorf("TelephoneResetPassword value empty: %s", value)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
}
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)
if value != code {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error")
}
@ -110,8 +96,8 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
// Don't fail register if device binding fails, just log the error
}
}
if l.ctx.Value(constant.CtxLoginType) != nil {
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
if l.ctx.Value(constant.LoginType) != nil {
req.LoginType = l.ctx.Value(constant.LoginType).(string)
}
// Generate session id
sessionId := uuidx.NewUUID().String()
@ -122,8 +108,7 @@ func (l *TelephoneResetPasswordLogic) TelephoneResetPassword(req *types.Telephon
l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId),
jwt.WithOption("identifier", req.Identifier),
jwt.WithOption("CtxLoginType", req.LoginType),
jwt.WithOption("LoginType", req.LoginType),
)
if err != nil {
l.Errorw("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -45,7 +45,6 @@ func NewTelephoneUserRegisterLogic(ctx context.Context, svcCtx *svc.ServiceConte
func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneRegisterRequest) (resp *types.LoginResponse, err error) {
c := l.svcCtx.Config.Register
var trialSubscribe *user.Subscribe
// Check if the registration is stopped
if c.StopRegister {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
@ -103,9 +102,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InviteCodeError), "invite code is invalid")
}
}
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "mobile", phoneNumber) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
}
// Generate password
pwd := tool.EncodePassWord(req.Password)
userInfo := &user.User{
@ -136,36 +133,12 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
}
if l.svcCtx.Config.Register.EnableTrial {
// Active trial
var trialErr error
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
if trialErr != nil {
return trialErr
if err = l.activeTrial(userInfo.Id); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
// Clear cache after transaction success
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
// Clear user subscription cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
// Don't return error, just log it
}
// Clear subscription cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
// Don't return error, just log it
}
// Clear all server cache
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
l.Errorf("ClearServerAllCache error: %v", err.Error())
// Don't return error, just log it
}
}
// Bind device to user if identifier is provided
if req.Identifier != "" {
@ -179,8 +152,8 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
// Don't fail register if device binding fails, just log the error
}
}
if l.ctx.Value(constant.CtxLoginType) != nil {
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
if l.ctx.Value(constant.LoginType) != nil {
req.LoginType = l.ctx.Value(constant.LoginType).(string)
}
// Generate session id
sessionId := uuidx.NewUUID().String()
@ -191,8 +164,7 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId),
jwt.WithOption("identifier", req.Identifier),
jwt.WithOption("CtxLoginType", req.LoginType),
jwt.WithOption("LoginType", req.LoginType),
)
if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
@ -254,10 +226,10 @@ func (l *TelephoneUserRegisterLogic) TelephoneUserRegister(req *types.TelephoneR
}, nil
}
func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) error {
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
if err != nil {
return nil, err
return err
}
userSub := &user.Subscribe{
Id: 0,
@ -273,10 +245,5 @@ func (l *TelephoneUserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, er
UUID: uuidx.NewUUID().String(),
Status: 1,
}
err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
if err != nil {
return nil, err
}
return userSub, nil
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
}

View File

@ -97,8 +97,8 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
// Don't fail login if device binding fails, just log the error
}
}
if l.ctx.Value(constant.CtxLoginType) != nil {
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
if l.ctx.Value(constant.LoginType) != nil {
req.LoginType = l.ctx.Value(constant.LoginType).(string)
}
// Generate session id
sessionId := uuidx.NewUUID().String()
@ -109,8 +109,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
l.svcCtx.Config.JwtAuth.AccessExpire,
jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId),
jwt.WithOption("identifier", req.Identifier),
jwt.WithOption("CtxLoginType", req.LoginType),
jwt.WithOption("LoginType", req.LoginType),
)
if err != nil {
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))

View File

@ -42,7 +42,6 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
c := l.svcCtx.Config.Register
email := l.svcCtx.Config.Email
var referer *user.User
var trialSubscribe *user.Subscribe
// Check if the registration is stopped
if c.StopRegister {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.StopRegister), "stop register")
@ -90,10 +89,6 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserDisabled), "user email deleted: %v", req.Email)
}
if !registerIpLimit(l.svcCtx, l.ctx, req.IP, "email", req.Email) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.RegisterIPLimit), "register ip limit: %v", req.IP)
}
// Generate password
pwd := tool.EncodePassWord(req.Password)
userInfo := &user.User{
@ -128,36 +123,12 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
if l.svcCtx.Config.Register.EnableTrial {
// Active trial
var trialErr error
trialSubscribe, trialErr = l.activeTrial(userInfo.Id)
if trialErr != nil {
return trialErr
if err = l.activeTrial(userInfo.Id); err != nil {
return err
}
}
return nil
})
if err != nil {
return nil, err
}
// Clear cache after transaction success
if l.svcCtx.Config.Register.EnableTrial && trialSubscribe != nil {
// Clear user subscription cache
if err = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, trialSubscribe); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("userSubscribeId", trialSubscribe.Id))
// Don't return error, just log it
}
// Clear subscription cache
if err = l.svcCtx.SubscribeModel.ClearCache(l.ctx, trialSubscribe.SubscribeId); err != nil {
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", trialSubscribe.SubscribeId))
// Don't return error, just log it
}
// Clear all server cache
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
l.Errorf("ClearServerAllCache error: %v", err.Error())
// Don't return error, just log it
}
}
// Bind device to user if identifier is provided
if req.Identifier != "" {
bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx)
@ -170,8 +141,8 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
// Don't fail register if device binding fails, just log the error
}
}
if l.ctx.Value(constant.CtxLoginType) != nil {
req.LoginType = l.ctx.Value(constant.CtxLoginType).(string)
if l.ctx.Value(constant.LoginType) != nil {
req.LoginType = l.ctx.Value(constant.LoginType).(string)
}
// Generate session id
sessionId := uuidx.NewUUID().String()
@ -245,10 +216,10 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
}, nil
}
func (l *UserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
func (l *UserRegisterLogic) activeTrial(uid int64) error {
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, l.svcCtx.Config.Register.TrialSubscribe)
if err != nil {
return nil, err
return err
}
userSub := &user.Subscribe{
UserId: uid,
@ -263,8 +234,5 @@ func (l *UserRegisterLogic) activeTrial(uid int64) (*user.Subscribe, error) {
UUID: uuidx.NewUUID().String(),
Status: 1,
}
if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil {
return nil, err
}
return userSub, nil
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub)
}

View File

@ -82,7 +82,7 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error {
l.Logger.Error("[AlipayNotify] Marshal payload failed", logger.Field("error", err.Error()))
return err
}
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
if err != nil {
l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error()))

View File

@ -84,7 +84,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
l.Logger.Error("[EPayNotify] Marshal payload failed", logger.Field("error", err.Error()))
return err
}
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
if err != nil {
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))

View File

@ -85,7 +85,7 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter)
l.Errorw("[StripeNotify] Marshal error", logger.Field("errors", err.Error()), logger.Field("payload", payload))
return err
}
task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
task := asynq.NewTask(types.ForthwithActivateOrder, bytes)
_, err = l.svcCtx.Queue.Enqueue(task)
if err != nil {
l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error()))

View File

@ -55,25 +55,6 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
// check subscribe plan quota limit
if sub.Quota > 0 {
userSub, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id)
if err != nil {
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscription error: %v", err.Error())
}
var count int64
for _, v := range userSub {
if v.SubscribeId == req.SubscribeId {
count++
}
}
if count >= sub.Quota {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
}
}
var discount float64 = 1
if sub.Discount != "" {
var dis []types.SubscribeDiscount
@ -117,6 +98,18 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
couponAmount = calculateCoupon(amount, couponInfo)
}
amount -= couponAmount
var deductionAmount int64
// Check user deduction amount
if u.GiftAmount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
}
}
var feeAmount int64
if req.Payment != 0 {
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
@ -127,19 +120,8 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
// Calculate the handling fee
if amount > 0 {
feeAmount = calculateFee(amount, payment)
amount += feeAmount
}
}
// Calculate gift amount deduction after fee calculation
var deductionAmount int64
if u.GiftAmount > 0 && amount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
}
amount += feeAmount
}
resp = &types.PreOrderResponse{

View File

@ -93,6 +93,19 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeOutOfStock), "subscribe out of stock")
}
// check subscribe plan limit
if sub.Quota > 0 {
var count int64
for _, v := range userSub {
if v.SubscribeId == req.SubscribeId {
count += 1
}
}
if count >= sub.Quota {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "quota limit")
}
}
var discount float64 = 1
if sub.Discount != "" {
var dis []types.SubscribeDiscount
@ -147,6 +160,19 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
}
// Calculate the handling fee
amount -= coupon
var deductionAmount int64
// Check user deduction amount
if u.GiftAmount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
u.GiftAmount -= deductionAmount
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
u.GiftAmount = 0
}
}
// find payment method
payment, err := l.svcCtx.PaymentModel.FindOne(l.ctx, req.Payment)
if err != nil {
@ -168,17 +194,6 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order amount exceeds maximum limit")
}
}
// Calculate gift amount deduction after fee calculation
var deductionAmount int64
if u.GiftAmount > 0 && amount > 0 {
if u.GiftAmount >= amount {
deductionAmount = amount
amount = 0
} else {
deductionAmount = u.GiftAmount
amount -= u.GiftAmount
}
}
// query user is new purchase or renewal
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil {
@ -206,28 +221,9 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
}
// Database transaction
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
// 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(&currentUserSub).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
// update user deduction && Pre deduction ,Return after canceling the order
if orderInfo.GiftAmount > 0 {
// deduct gift amount from user
u.GiftAmount -= orderInfo.GiftAmount
// update user deduction && Pre deduction ,Return after canceling the order
if e := l.svcCtx.UserModel.Update(l.ctx, u, db); e != nil {
l.Errorw("[Purchase] Database update error", logger.Field("error", e.Error()), logger.Field("user", u))
return e

View File

@ -366,7 +366,6 @@ func (l *PurchaseCheckoutLogic) CryptoSaaSPayment(config *payment.Payment, info
}
notifyUrl = notifyUrl + "/v1/notify/" + config.Platform + "/" + config.Token
}
// Create payment URL for user redirection
url := client.CreatePayUrl(epay.Order{
Name: l.svcCtx.Config.Site.SiteName,

View File

@ -150,7 +150,7 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
}
content, _ := tempOrder.Marshal()
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), 24*time.Hour).Result(); err != nil {
if _, err = l.svcCtx.Redis.Set(l.ctx, fmt.Sprintf(constant.TempOrderCacheKey, orderInfo.OrderNo), string(content), CloseOrderTimeMinutes*time.Minute).Result(); err != nil {
l.Errorw("[Purchase] Redis set error", logger.Field("error", err.Error()), logger.Field("order_no", orderInfo.OrderNo))
return err
}

View File

@ -1,224 +0,0 @@
package redemption
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
queue "github.com/perfect-panel/server/queue/types"
"github.com/pkg/errors"
"gorm.io/gorm"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/logger"
)
type RedeemCodeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Redeem code
func NewRedeemCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RedeemCodeLogic {
return &RedeemCodeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *RedeemCodeLogic) RedeemCode(req *types.RedeemCodeRequest) (resp *types.RedeemCodeResponse, err error) {
// Get user from context
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// 使用Redis分布式锁防止并发重复兑换
lockKey := fmt.Sprintf("redemption_lock:%d:%s", u.Id, req.Code)
lockSuccess, err := l.svcCtx.Redis.SetNX(l.ctx, lockKey, "1", 10*time.Second).Result()
if err != nil {
l.Errorw("[RedeemCode] Acquire lock failed", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "system busy, please try again later")
}
if !lockSuccess {
l.Errorw("[RedeemCode] Redemption in progress",
logger.Field("user_id", u.Id),
logger.Field("code", req.Code))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "redemption in progress, please wait")
}
defer l.svcCtx.Redis.Del(l.ctx, lockKey)
// Find redemption code by code
redemptionCode, err := l.svcCtx.RedemptionCodeModel.FindOneByCode(l.ctx, req.Code)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("[RedeemCode] Redemption code not found", logger.Field("code", req.Code))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code not found")
}
l.Errorw("[RedeemCode] Database Error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption code error: %v", err.Error())
}
// Check if redemption code is enabled
if redemptionCode.Status != 1 {
l.Errorw("[RedeemCode] Redemption code is disabled",
logger.Field("code", req.Code),
logger.Field("status", redemptionCode.Status))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code is disabled")
}
// Check if redemption code has remaining count
if redemptionCode.TotalCount > 0 && redemptionCode.UsedCount >= redemptionCode.TotalCount {
l.Errorw("[RedeemCode] Redemption code has been fully used",
logger.Field("code", req.Code),
logger.Field("total_count", redemptionCode.TotalCount),
logger.Field("used_count", redemptionCode.UsedCount))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "redemption code has been fully used")
}
// Check if user has already redeemed this code
userRecords, err := l.svcCtx.RedemptionRecordModel.FindByUserId(l.ctx, u.Id)
if err != nil {
l.Errorw("[RedeemCode] Database Error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find redemption records error: %v", err.Error())
}
for _, record := range userRecords {
if record.RedemptionCodeId == redemptionCode.Id {
l.Errorw("[RedeemCode] User has already redeemed this code",
logger.Field("user_id", u.Id),
logger.Field("code", req.Code))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "you have already redeemed this code")
}
}
// Find subscribe plan from redemption code
subscribePlan, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, redemptionCode.SubscribePlan)
if err != nil {
l.Errorw("[RedeemCode] Subscribe plan not found",
logger.Field("subscribe_plan", redemptionCode.SubscribePlan),
logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "subscribe plan not found")
}
// Check if subscribe plan is available
if !*subscribePlan.Sell {
l.Errorw("[RedeemCode] Subscribe plan is not available",
logger.Field("subscribe_plan", redemptionCode.SubscribePlan))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe plan is not available")
}
// 检查配额限制(预检查,队列任务中会再次检查)
if subscribePlan.Quota > 0 {
var count int64
err = l.svcCtx.DB.Model(&user.Subscribe{}).
Where("user_id = ? AND subscribe_id = ?", u.Id, redemptionCode.SubscribePlan).
Count(&count).Error
if err != nil {
l.Errorw("[RedeemCode] Check quota failed", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "check quota failed")
}
if count >= subscribePlan.Quota {
l.Errorw("[RedeemCode] Subscribe quota limit exceeded",
logger.Field("user_id", u.Id),
logger.Field("subscribe_id", redemptionCode.SubscribePlan),
logger.Field("quota", subscribePlan.Quota),
logger.Field("current_count", count))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeQuotaLimit), "subscribe quota limit exceeded")
}
}
// 判断是否首次购买
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil {
l.Errorw("[RedeemCode] Check user order failed", logger.Field("error", err.Error()))
// 可以继续默认为false
isNew = false
}
// 创建Order记录
orderInfo := &order.Order{
UserId: u.Id,
OrderNo: tool.GenerateTradeNo(),
Type: 5, // 兑换类型
Quantity: redemptionCode.Quantity,
Price: 0, // 兑换无价格
Amount: 0, // 兑换无金额
Discount: 0,
GiftAmount: 0,
Coupon: "",
CouponDiscount: 0,
PaymentId: 0,
Method: "redemption",
FeeAmount: 0,
Commission: 0,
Status: 2, // 直接设置为已支付
SubscribeId: redemptionCode.SubscribePlan,
IsNew: isNew,
}
// 保存Order到数据库
err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo)
if err != nil {
l.Errorw("[RedeemCode] Create order failed", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create order failed")
}
// 缓存兑换码信息到Redis供队列任务使用
cacheKey := fmt.Sprintf("redemption_order:%s", orderInfo.OrderNo)
cacheData := map[string]interface{}{
"redemption_code_id": redemptionCode.Id,
"unit_time": redemptionCode.UnitTime,
"quantity": redemptionCode.Quantity,
}
jsonData, _ := json.Marshal(cacheData)
err = l.svcCtx.Redis.Set(l.ctx, cacheKey, jsonData, 2*time.Hour).Err()
if err != nil {
l.Errorw("[RedeemCode] Cache redemption data failed", logger.Field("error", err.Error()))
// 缓存失败删除已创建的Order避免孤儿记录
if delErr := l.svcCtx.OrderModel.Delete(l.ctx, orderInfo.Id); delErr != nil {
l.Errorw("[RedeemCode] Delete order failed after cache error",
logger.Field("order_id", orderInfo.Id),
logger.Field("error", delErr.Error()))
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "cache redemption data failed")
}
// 触发队列任务
payload := queue.ForthwithActivateOrderPayload{
OrderNo: orderInfo.OrderNo,
}
bytes, _ := json.Marshal(&payload)
task := asynq.NewTask(queue.ForthwithActivateOrder, bytes, asynq.MaxRetry(5))
_, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task)
if err != nil {
l.Errorw("[RedeemCode] Enqueue task failed", logger.Field("error", err.Error()))
// 入队失败删除Order和Redis缓存
l.svcCtx.Redis.Del(l.ctx, cacheKey)
if delErr := l.svcCtx.OrderModel.Delete(l.ctx, orderInfo.Id); delErr != nil {
l.Errorw("[RedeemCode] Delete order failed after enqueue error",
logger.Field("order_id", orderInfo.Id),
logger.Field("error", delErr.Error()))
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enqueue task failed")
}
l.Infow("[RedeemCode] Redemption order created successfully",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("user_id", u.Id),
)
return &types.RedeemCodeResponse{
Message: "Redemption successful, processing...",
}, nil
}

View File

@ -38,7 +38,7 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 0, 1, 2, 3)
userSubscribes, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, u.Id, 1, 2)
if err != nil {
logger.Errorw("failed to query user subscribe", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "DB_ERROR")
@ -79,6 +79,7 @@ func (l *QueryUserSubscribeNodeListLogic) QueryUserSubscribeNodeList() (resp *ty
if l.svcCtx.Config.Register.EnableTrial && l.svcCtx.Config.Register.TrialSubscribe == userSubscribe.SubscribeId {
userSubscribeInfo.IsTryOut = true
}
resp.List = append(resp.List, userSubscribeInfo)
}
@ -136,21 +137,16 @@ func (l *QueryUserSubscribeNodeListLogic) getServers(userSub *user.Subscribe) (u
continue
}
userSubscribeNode := &types.UserSubscribeNodeInfo{
Id: n.Id,
Name: n.Name,
Uuid: userSub.UUID,
Protocol: n.Protocol,
Protocols: server.Protocols,
Port: n.Port,
Address: n.Address,
Tags: strings.Split(n.Tags, ","),
Country: server.Country,
City: server.City,
Latitude: server.Latitude,
Longitude: server.Longitude,
LongitudeCenter: server.LongitudeCenter,
LatitudeCenter: server.LatitudeCenter,
CreatedAt: n.CreatedAt.Unix(),
Id: n.Id,
Name: n.Name,
Uuid: userSub.UUID,
Protocol: n.Protocol,
Port: n.Port,
Address: n.Address,
Tags: strings.Split(n.Tags, ","),
Country: server.Country,
City: server.City,
CreatedAt: n.CreatedAt.Unix(),
}
userSubscribeNodes = append(userSubscribeNodes, userSubscribeNode)
}

View File

@ -1,86 +0,0 @@
package user
import (
"context"
"fmt"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
)
type DeleteCurrentUserAccountLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Delete Current User Account
func NewDeleteCurrentUserAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCurrentUserAccountLogic {
return &DeleteCurrentUserAccountLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteCurrentUserAccountLogic) DeleteCurrentUserAccount() (err error) {
userInfo, exists := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !exists {
return nil
}
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, userInfo.Id)
if err != nil {
l.Errorw("FindOne Error", logger.Field("error", err))
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth methods failed: %v", err.Error())
}
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
//delete user devices
if len(userInfo.UserDevices) > 0 {
for _, device := range userInfo.UserDevices {
if err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id, tx); err != nil {
return err
}
}
}
// delete user auth methods
if len(userInfo.AuthMethods) > 0 {
for _, authMethod := range userInfo.AuthMethods {
if err = l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, userInfo.Id, authMethod.AuthType); err != nil {
return err
}
}
}
// delete user subscribes
var subscribes []*user.SubscribeDetails
subscribes, err = l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id)
if err != nil {
return err
}
for _, subscribe := range subscribes {
if err = l.svcCtx.UserModel.DeleteSubscribe(l.ctx, subscribe.Token, tx); err != nil {
return err
}
}
// delete user account
return l.svcCtx.UserModel.BatchDeleteUser(l.ctx, []int64{userInfo.Id}, tx)
})
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "find user auth methods failed: %v", err.Error())
}
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, l.ctx.Value(constant.CtxKeySessionID))
if err = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err(); err != nil {
l.Logger.Errorf("delete session id cache failed: %v", err.Error())
}
return
}

View File

@ -1,115 +0,0 @@
package user
import (
"context"
"sort"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
)
type DeviceOnlineStatisticsLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Device Online Statistics
func NewDeviceOnlineStatisticsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceOnlineStatisticsLogic {
return &DeviceOnlineStatisticsLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeviceOnlineStatisticsLogic) DeviceOnlineStatistics() (resp *types.GetDeviceOnlineStatsResponse, err error) {
u := l.ctx.Value(constant.CtxKeyUser).(*user.User)
//获取历史最长在线时间
var OnlineSeconds int64
if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("online_seconds").Order("online_seconds desc").Limit(1).Scan(&OnlineSeconds).Error; err != nil {
l.Logger.Error(err)
}
//获取历史连续最长在线天数
var DurationDays int64
if err := l.svcCtx.DB.Model(user.DeviceOnlineRecord{}).Where("user_id = ?", u.Id).Select("duration_days").Order("duration_days desc").Limit(1).Scan(&DurationDays).Error; err != nil {
l.Logger.Error(err)
}
//获取近七天在线情况
var userOnlineRecord []user.DeviceOnlineRecord
if err := l.svcCtx.DB.Model(&userOnlineRecord).Where("user_id = ? and created_at >= ?", u.Id, time.Now().AddDate(0, 0, -7).Format(time.DateTime)).Order("created_at desc").Find(&userOnlineRecord).Error; err != nil {
l.Logger.Error(err)
}
//获取当前连续在线天数
var currentContinuousDays int64
if len(userOnlineRecord) > 0 {
currentContinuousDays = userOnlineRecord[0].DurationDays
} else {
currentContinuousDays = 1
}
var dates []string
for i := 0; i < 7; i++ {
date := time.Now().AddDate(0, 0, -i).Format(time.DateOnly)
dates = append(dates, date)
}
onlineDays := make(map[string]types.WeeklyStat)
for _, record := range userOnlineRecord {
//获取近七天在线情况
onlineTime := record.OnlineTime.Format(time.DateOnly)
if weeklyStat, ok := onlineDays[onlineTime]; ok {
weeklyStat.Hours += float64(record.OnlineSeconds)
onlineDays[onlineTime] = weeklyStat
} else {
onlineDays[onlineTime] = types.WeeklyStat{
Hours: float64(record.OnlineSeconds),
//根据日期获取周几
DayName: record.OnlineTime.Weekday().String(),
}
}
}
//补全不存在的日期
for _, date := range dates {
if _, ok := onlineDays[date]; !ok {
onlineTime, _ := time.Parse(time.DateOnly, date)
onlineDays[date] = types.WeeklyStat{
DayName: onlineTime.Weekday().String(),
}
}
}
var keys []string
for key := range onlineDays {
keys = append(keys, key)
}
//排序
sort.Strings(keys)
var weeklyStats []types.WeeklyStat
for index, key := range keys {
weeklyStat := onlineDays[key]
weeklyStat.Day = index + 1
weeklyStat.Hours = weeklyStat.Hours / float64(3600)
weeklyStats = append(weeklyStats, weeklyStat)
}
resp = &types.GetDeviceOnlineStatsResponse{
WeeklyStats: weeklyStats,
ConnectionRecords: types.ConnectionRecords{
CurrentContinuousDays: currentContinuousDays,
HistoryContinuousDays: DurationDays,
LongestSingleConnection: OnlineSeconds / 60,
},
}
return
}

View File

@ -83,9 +83,6 @@ func (l *ResetUserSubscribeTokenLogic) ResetUserSubscribeToken(req *types.ResetU
l.Errorw("ClearSubscribeCache failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSub.SubscribeId))
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
}

View File

@ -64,23 +64,9 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
}
var count int64
err = tx.Model(user.AuthMethods{}).Where("user_id = ?", deleteDevice.UserId).Count(&count).Error
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count user auth methods err: %v", err)
}
if count < 1 {
_ = tx.Where("id = ?", deleteDevice.UserId).Delete(&user.User{}).Error
}
//remove device cache
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, deleteDevice.Identifier)
if sessionId, err := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); err == nil && sessionId != "" {
_ = l.svcCtx.Redis.Del(l.ctx, deviceCacheKey).Err()
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
_ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err()
}
sessionId := l.ctx.Value(constant.CtxKeySessionID)
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey)
return nil
})
}

View File

@ -1,87 +0,0 @@
package ws
import (
"context"
sysErr "errors"
"time"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
)
type DeviceWsConnectLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Webosocket Device Connect
func NewDeviceWsConnectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeviceWsConnectLogic {
return &DeviceWsConnectLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeviceWsConnectLogic) DeviceWsConnect(c *gin.Context) error {
value := l.ctx.Value(constant.CtxKeyIdentifier)
if value == nil || value.(string) == "" {
value, _ = c.GetQuery("identifier")
if value == nil || value.(string) == "" {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect identifier is empty")
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "identifier is empty")
}
}
identifier := value.(string)
_, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
if err != nil && !sysErr.Is(err, gorm.ErrRecordNotFound) {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect FindOneDeviceByIdentifier err: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), err.Error())
}
value = l.ctx.Value(constant.CtxKeyUser)
if value == nil {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect value is nil")
return nil
}
userInfo := value.(*user.User)
if sysErr.Is(err, gorm.ErrRecordNotFound) {
device := user.Device{
Identifier: identifier,
UserId: userInfo.Id,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
Online: true,
Enabled: true,
}
err := l.svcCtx.UserModel.InsertDevice(l.ctx, &device)
if err != nil {
l.Errorf("DeviceWsConnectLogic DeviceWsConnect InsertDevice err: %v", err)
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), err.Error())
}
}
//默认在线设备1
maxDevice := 3
subscribe, err := l.svcCtx.UserModel.QueryUserSubscribe(l.ctx, userInfo.Id, 1, 2)
if err == nil {
for _, sub := range subscribe {
if time.Now().Before(sub.ExpireTime) {
deviceLimit := int(sub.Subscribe.DeviceLimit)
if deviceLimit > maxDevice {
maxDevice = deviceLimit
}
}
}
}
l.svcCtx.DeviceManager.AddDevice(c.Writer, c.Request, l.ctx.Value(constant.CtxKeySessionID).(string), userInfo.Id, identifier, maxDevice)
return nil
}

View File

@ -42,11 +42,8 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
}
loginType := ""
if claims["CtxLoginType"] != nil {
loginType = claims["CtxLoginType"].(string)
}
if claims["identifier"] != nil {
ctx = context.WithValue(ctx, constant.CtxKeyIdentifier, claims["identifier"].(string))
if claims["LoginType"] != nil {
loginType = claims["LoginType"].(string)
}
// get user id from token
userId := int64(claims["UserId"].(float64))
@ -85,10 +82,9 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
c.Abort()
return
}
ctx = context.WithValue(ctx, constant.CtxLoginType, loginType)
ctx = context.WithValue(ctx, constant.LoginType, loginType)
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
c.Request = c.Request.WithContext(ctx)
c.Next()
}

View File

@ -42,11 +42,11 @@ func DeviceMiddleware(srvCtx *svc.ServiceContext) func(c *gin.Context) {
ctx := c.Request.Context()
if ctx.Value(constant.CtxKeyUser) == nil && c.GetHeader("Login-Type") != "" {
ctx = context.WithValue(ctx, constant.CtxLoginType, c.GetHeader("Login-Type"))
ctx = context.WithValue(ctx, constant.LoginType, c.GetHeader("Login-Type"))
c.Request = c.Request.WithContext(ctx)
}
loginType, ok := ctx.Value(constant.CtxLoginType).(string)
loginType, ok := ctx.Value(constant.LoginType).(string)
if !ok || loginType != "device" {
c.Next()
return

View File

@ -27,9 +27,6 @@ type Filter struct {
// GetAnnouncementListByPage get announcement list by page
func (m *customAnnouncementModel) GetAnnouncementListByPage(ctx context.Context, page, size int, filter Filter) (int64, []*Announcement, error) {
if size == 0 {
size = 10
}
var list []*Announcement
var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {

View File

@ -13,7 +13,6 @@ type customServerLogicModel interface {
FilterServerList(ctx context.Context, params *FilterParams) (int64, []*Server, error)
FilterNodeList(ctx context.Context, params *FilterNodeParams) (int64, []*Node, error)
ClearNodeCache(ctx context.Context, params *FilterNodeParams) error
ClearServerAllCache(ctx context.Context) error
}
const (
@ -172,30 +171,6 @@ func (m *customServerModel) ClearServerCache(ctx context.Context, serverId int64
return nil
}
func (m *customServerModel) ClearServerAllCache(ctx context.Context) error {
var cursor uint64
var keys []string
prefix := ServerUserListCacheKey + "*"
for {
scanKeys, newCursor, err := m.Cache.Scan(ctx, cursor, prefix, 999).Result()
if err != nil {
m.Logger.Error(ctx, fmt.Sprintf("ClearServerAllCache err:%v", err))
break
}
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache query keys:%v", scanKeys))
keys = append(keys, scanKeys...)
cursor = newCursor
if cursor == 0 {
break
}
}
if len(keys) > 0 {
m.Logger.Info(ctx, fmt.Sprintf("ClearServerAllCache keys:%v", keys))
return m.Cache.Del(ctx, keys...).Err()
}
return nil
}
// InSet 支持多值 OR 查询
func InSet(field string, values []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {

View File

@ -15,16 +15,12 @@ type Server struct {
Country string `gorm:"type:varchar(128);not null;default:'';comment:Country"`
City string `gorm:"type:varchar(128);not null;default:'';comment:City"`
//Ratio float32 `gorm:"type:DECIMAL(4,2);not null;default:0;comment:Traffic Ratio"`
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
Longitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Longitude"`
Latitude string `gorm:"type:varchar(50);not null;default:'0.0';comment:Latitude"`
LongitudeCenter string `gorm:"type:varchar(50);not null;default:'0.0';comment:Center Longitude"`
LatitudeCenter string `gorm:"type:varchar(50);not null;default:'0.0';comment:Center Latitude"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
Address string `gorm:"type:varchar(100);not null;default:'';comment:Server Address"`
Sort int `gorm:"type:int;not null;default:0;comment:Sort"`
Protocols string `gorm:"type:text;default:null;comment:Protocol"`
LastReportedAt *time.Time `gorm:"comment:Last Reported Time"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (*Server) TableName() string {

View File

@ -1,288 +0,0 @@
package redemption
import (
"context"
"errors"
"fmt"
"github.com/perfect-panel/server/pkg/cache"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
var _ RedemptionCodeModel = (*customRedemptionCodeModel)(nil)
var _ RedemptionRecordModel = (*customRedemptionRecordModel)(nil)
var (
cacheRedemptionCodeIdPrefix = "cache:redemption_code:id:"
cacheRedemptionCodeCodePrefix = "cache:redemption_code:code:"
cacheRedemptionRecordIdPrefix = "cache:redemption_record:id:"
)
type (
RedemptionCodeModel interface {
Insert(ctx context.Context, data *RedemptionCode) error
FindOne(ctx context.Context, id int64) (*RedemptionCode, error)
FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error)
Update(ctx context.Context, data *RedemptionCode, tx ...*gorm.DB) error
Delete(ctx context.Context, id int64) error
Transaction(ctx context.Context, fn func(db *gorm.DB) error) error
customRedemptionCodeLogicModel
}
RedemptionRecordModel interface {
Insert(ctx context.Context, data *RedemptionRecord, tx ...*gorm.DB) error
FindOne(ctx context.Context, id int64) (*RedemptionRecord, error)
Update(ctx context.Context, data *RedemptionRecord) error
Delete(ctx context.Context, id int64) error
customRedemptionRecordLogicModel
}
customRedemptionCodeLogicModel interface {
QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error)
BatchDelete(ctx context.Context, ids []int64) error
IncrementUsedCount(ctx context.Context, id int64, tx ...*gorm.DB) error
}
customRedemptionRecordLogicModel interface {
QueryRedemptionRecordListByPage(ctx context.Context, page, size int, userId int64, codeId int64) (total int64, list []*RedemptionRecord, err error)
FindByUserId(ctx context.Context, userId int64) ([]*RedemptionRecord, error)
FindByCodeId(ctx context.Context, codeId int64) ([]*RedemptionRecord, error)
}
customRedemptionCodeModel struct {
*defaultRedemptionCodeModel
}
defaultRedemptionCodeModel struct {
cache.CachedConn
table string
}
customRedemptionRecordModel struct {
*defaultRedemptionRecordModel
}
defaultRedemptionRecordModel struct {
cache.CachedConn
table string
}
)
func newRedemptionCodeModel(db *gorm.DB, c *redis.Client) *defaultRedemptionCodeModel {
return &defaultRedemptionCodeModel{
CachedConn: cache.NewConn(db, c),
table: "`redemption_code`",
}
}
func newRedemptionRecordModel(db *gorm.DB, c *redis.Client) *defaultRedemptionRecordModel {
return &defaultRedemptionRecordModel{
CachedConn: cache.NewConn(db, c),
table: "`redemption_record`",
}
}
// RedemptionCode cache methods
func (m *defaultRedemptionCodeModel) getCacheKeys(data *RedemptionCode) []string {
if data == nil {
return []string{}
}
codeIdKey := fmt.Sprintf("%s%v", cacheRedemptionCodeIdPrefix, data.Id)
codeCodeKey := fmt.Sprintf("%s%v", cacheRedemptionCodeCodePrefix, data.Code)
cacheKeys := []string{
codeIdKey,
codeCodeKey,
}
return cacheKeys
}
func (m *defaultRedemptionCodeModel) Insert(ctx context.Context, data *RedemptionCode) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
return conn.Create(data).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultRedemptionCodeModel) FindOne(ctx context.Context, id int64) (*RedemptionCode, error) {
codeIdKey := fmt.Sprintf("%s%v", cacheRedemptionCodeIdPrefix, id)
var resp RedemptionCode
err := m.QueryCtx(ctx, &resp, codeIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&RedemptionCode{}).Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultRedemptionCodeModel) FindOneByCode(ctx context.Context, code string) (*RedemptionCode, error) {
codeCodeKey := fmt.Sprintf("%s%v", cacheRedemptionCodeCodePrefix, code)
var resp RedemptionCode
err := m.QueryCtx(ctx, &resp, codeCodeKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&RedemptionCode{}).Where("`code` = ?", code).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultRedemptionCodeModel) Update(ctx context.Context, data *RedemptionCode, tx ...*gorm.DB) error {
old, err := m.FindOne(ctx, data.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
if len(tx) > 0 {
db = tx[0]
}
return db.Save(data).Error
}, m.getCacheKeys(old)...)
return err
}
func (m *defaultRedemptionCodeModel) Delete(ctx context.Context, id int64) error {
data, err := m.FindOne(ctx, id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
err = m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Delete(&RedemptionCode{}, id).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultRedemptionCodeModel) Transaction(ctx context.Context, fn func(db *gorm.DB) error) error {
return m.TransactCtx(ctx, fn)
}
// RedemptionCode custom logic methods
func (m *customRedemptionCodeModel) QueryRedemptionCodeListByPage(ctx context.Context, page, size int, subscribePlan int64, unitTime string, code string) (total int64, list []*RedemptionCode, err error) {
err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
db := conn.Model(&RedemptionCode{})
if subscribePlan != 0 {
db = db.Where("subscribe_plan = ?", subscribePlan)
}
if unitTime != "" {
db = db.Where("unit_time = ?", unitTime)
}
if code != "" {
db = db.Where("code like ?", "%"+code+"%")
}
return db.Count(&total).Limit(size).Offset((page - 1) * size).Order("created_at DESC").Find(v).Error
})
return total, list, err
}
func (m *customRedemptionCodeModel) BatchDelete(ctx context.Context, ids []int64) error {
var err error
for _, id := range ids {
if err = m.Delete(ctx, id); err != nil {
return err
}
}
return nil
}
func (m *customRedemptionCodeModel) IncrementUsedCount(ctx context.Context, id int64, tx ...*gorm.DB) error {
data, err := m.FindOne(ctx, id)
if err != nil {
return err
}
data.UsedCount++
if len(tx) > 0 {
return m.Update(ctx, data, tx[0])
}
return m.Update(ctx, data)
}
// RedemptionRecord cache methods
func (m *defaultRedemptionRecordModel) getCacheKeys(data *RedemptionRecord) []string {
if data == nil {
return []string{}
}
recordIdKey := fmt.Sprintf("%s%v", cacheRedemptionRecordIdPrefix, data.Id)
cacheKeys := []string{
recordIdKey,
}
return cacheKeys
}
func (m *defaultRedemptionRecordModel) Insert(ctx context.Context, data *RedemptionRecord, tx ...*gorm.DB) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
if len(tx) > 0 {
conn = tx[0]
}
return conn.Create(data).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultRedemptionRecordModel) FindOne(ctx context.Context, id int64) (*RedemptionRecord, error) {
recordIdKey := fmt.Sprintf("%s%v", cacheRedemptionRecordIdPrefix, id)
var resp RedemptionRecord
err := m.QueryCtx(ctx, &resp, recordIdKey, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&RedemptionRecord{}).Where("`id` = ?", id).First(&resp).Error
})
switch {
case err == nil:
return &resp, nil
default:
return nil, err
}
}
func (m *defaultRedemptionRecordModel) Update(ctx context.Context, data *RedemptionRecord) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Save(data).Error
}, m.getCacheKeys(data)...)
return err
}
func (m *defaultRedemptionRecordModel) Delete(ctx context.Context, id int64) error {
err := m.ExecCtx(ctx, func(conn *gorm.DB) error {
db := conn
return db.Delete(&RedemptionRecord{}, id).Error
}, m.getCacheKeys(nil)...)
return err
}
// RedemptionRecord custom logic methods
func (m *customRedemptionRecordModel) QueryRedemptionRecordListByPage(ctx context.Context, page, size int, userId int64, codeId int64) (total int64, list []*RedemptionRecord, err error) {
err = m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
db := conn.Model(&RedemptionRecord{})
if userId != 0 {
db = db.Where("user_id = ?", userId)
}
if codeId != 0 {
db = db.Where("redemption_code_id = ?", codeId)
}
return db.Count(&total).Limit(size).Offset((page - 1) * size).Order("created_at DESC").Find(v).Error
})
return total, list, err
}
func (m *customRedemptionRecordModel) FindByUserId(ctx context.Context, userId int64) ([]*RedemptionRecord, error) {
var list []*RedemptionRecord
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&RedemptionRecord{}).Where("user_id = ?", userId).Order("created_at DESC").Find(v).Error
})
return list, err
}
func (m *customRedemptionRecordModel) FindByCodeId(ctx context.Context, codeId int64) ([]*RedemptionRecord, error) {
var list []*RedemptionRecord
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&RedemptionRecord{}).Where("redemption_code_id = ?", codeId).Order("created_at DESC").Find(v).Error
})
return list, err
}

View File

@ -1,20 +0,0 @@
package redemption
import (
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
// NewRedemptionCodeModel returns a model for the redemption_code table.
func NewRedemptionCodeModel(conn *gorm.DB, c *redis.Client) RedemptionCodeModel {
return &customRedemptionCodeModel{
defaultRedemptionCodeModel: newRedemptionCodeModel(conn, c),
}
}
// NewRedemptionRecordModel returns a model for the redemption_record table.
func NewRedemptionRecordModel(conn *gorm.DB, c *redis.Client) RedemptionRecordModel {
return &customRedemptionRecordModel{
defaultRedemptionRecordModel: newRedemptionRecordModel(conn, c),
}
}

View File

@ -1,40 +0,0 @@
package redemption
import (
"time"
"gorm.io/gorm"
)
type RedemptionCode struct {
Id int64 `gorm:"primaryKey"`
Code string `gorm:"type:varchar(255);not null;unique;comment:Redemption Code"`
TotalCount int64 `gorm:"type:int;not null;default:0;comment:Total Redemption Count"`
UsedCount int64 `gorm:"type:int;not null;default:0;comment:Used Redemption Count"`
SubscribePlan int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Plan"`
UnitTime string `gorm:"type:varchar(50);not null;default:'month';comment:Unit Time: day, month, quarter, half_year, year"`
Quantity int64 `gorm:"type:int;not null;default:1;comment:Quantity"`
Status int64 `gorm:"type:tinyint;not null;default:1;comment:Status: 1=enabled, 0=disabled"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
DeletedAt gorm.DeletedAt `gorm:"index;comment:Delete Time"`
}
type RedemptionRecord struct {
Id int64 `gorm:"primaryKey"`
RedemptionCodeId int64 `gorm:"type:bigint;not null;default:0;comment:Redemption Code Id;index"`
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id;index"`
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"`
UnitTime string `gorm:"type:varchar(50);not null;default:'month';comment:Unit Time"`
Quantity int64 `gorm:"type:int;not null;default:1;comment:Quantity"`
RedeemedAt time.Time `gorm:"<-:create;comment:Redeemed Time"`
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
}
func (RedemptionCode) TableName() string {
return "redemption_code"
}
func (RedemptionRecord) TableName() string {
return "redemption_record"
}

View File

@ -61,7 +61,6 @@ type UserFilterParams struct {
UserId *int64
SubscribeId *int64
UserSubscribeId *int64
ShortCode string
Order string // Order by id, e.g., "desc"
Unscoped bool // Whether to include soft-deleted records
}
@ -147,10 +146,6 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
conn = conn.Joins("LEFT JOIN user_subscribe ON user.id = user_subscribe.user_id").
Where("user_subscribe.subscribe_id =? and `status` IN (0,1)", *filter.SubscribeId)
}
if filter.ShortCode != "" {
conn = conn.Joins("LEFT JOIN user_device ON user.id = user_device.user_id").
Where("user_device.short_code LIKE ?", "%"+filter.ShortCode+"%")
}
if filter.Order != "" {
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
}
@ -158,7 +153,7 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
conn = conn.Unscoped()
}
}
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page-1)*size).Preload("UserDevices").Preload("AuthMethods", func(db *gorm.DB) *gorm.DB { return db.Order("user_auth_methods.auth_type desc") }).Find(&list).Error
return conn.Model(&User{}).Group("user.id").Count(&total).Limit(size).Offset((page - 1) * size).Preload("UserDevices").Preload("AuthMethods").Find(&list).Error
})
return list, total, err
}
@ -235,7 +230,7 @@ func (m *customUserModel) QueryResisterUserTotal(ctx context.Context) (int64, er
func (m *customUserModel) QueryAdminUsers(ctx context.Context) ([]*User, error) {
var data []*User
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&User{}).Preload("AuthMethods", func(db *gorm.DB) *gorm.DB { return db.Order("user_auth_methods.auth_type desc") }).Where("is_admin = ?", true).Find(&data).Error
return conn.Model(&User{}).Preload("AuthMethods").Where("is_admin = ?", true).Find(&data).Error
})
return data, err
}

Some files were not shown because too many files have changed in this diff Show More