Compare commits
79 Commits
old
...
b650b1a108
| Author | SHA1 | Date | |
|---|---|---|---|
| b650b1a108 | |||
| efc5be5142 | |||
| 5c43bd7a3d | |||
| 30bbb33129 | |||
| 66e753e69b | |||
| 93fcaace26 | |||
| 97bb873bb8 | |||
| 723ceddc3d | |||
| be532bec1b | |||
| e788693713 | |||
| aaa6cb149c | |||
| 6a146f0898 | |||
| b9b0dc3bda | |||
| 4d39fd1b84 | |||
| bcf81ab683 | |||
| 7471fd8e3d | |||
| ac3bbaf7bf | |||
| 2d4bfe0330 | |||
| 602ecc31bc | |||
| b16ac2cb9b | |||
| 41423ca666 | |||
| e898e6afbe | |||
| 2e01ba0e3d | |||
| 25db7ea428 | |||
| e37ad50410 | |||
| c503e06c9c | |||
| 0992c2ad7b | |||
| e78b93d6fb | |||
| 79020ccea0 | |||
| 8a8ece76b1 | |||
| 0316345e9f | |||
| b56b1240f3 | |||
| 77b3af8015 | |||
| 634f43d644 | |||
| e5b4dc75d7 | |||
| 40b8c07036 | |||
| b261ddeeb1 | |||
| 62f07b7f6b | |||
| 31a2aa0e84 | |||
| 7965c33790 | |||
| fcae874fd2 | |||
| dad83339c1 | |||
| 84842ec3dd | |||
| 59bc008b69 | |||
| 8cb58ad91f | |||
| 7372302618 | |||
| cbf5a2b450 | |||
| 33f450cdbc | |||
| bce4c4e56e | |||
| b6cbcb3040 | |||
| fdbe3c75ee | |||
| bf13c56261 | |||
| 85343f3f82 | |||
| 51b493a10d | |||
| ae04e0d518 | |||
| 2b1ce740e1 | |||
| e620abcce6 | |||
| 7ea6fc226e | |||
| d5f4b04c6c | |||
| a0ca976e66 | |||
| 917732eeee | |||
| f0fbd9d208 | |||
| 2d07182bd6 | |||
| e0ae15355b | |||
| c0e0d03431 | |||
| bb5d6974fa | |||
| df7fcddb40 | |||
| 6c9d5fb39e | |||
| 4ecc5177ed | |||
| c11d46bdbf | |||
| 9b7a588997 | |||
| 0f40c63ea0 | |||
| 1d1d8c0e30 | |||
| 5d890fdfcc | |||
| 94e2f33db1 | |||
| a7d9deb9cd | |||
| 9f9e832e37 | |||
| a73a3f2313 | |||
| cc6ebc18e5 |
@@ -0,0 +1,231 @@
|
|||||||
|
name: Build docker and publish
|
||||||
|
run-name: 简化的Docker构建和部署流程
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Docker镜像仓库
|
||||||
|
REPO: ${{ vars.REPO || 'registry.kxsw.us/ppanel-server' }}
|
||||||
|
# SSH连接信息
|
||||||
|
SSH_HOST: ${{ vars.SSH_HOST }}
|
||||||
|
SSH_PORT: ${{ vars.SSH_PORT }}
|
||||||
|
SSH_USER: ${{ vars.SSH_USER }}
|
||||||
|
SSH_PASSWORD: ${{ vars.SSH_PASSWORD }}
|
||||||
|
# TG通知
|
||||||
|
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
||||||
|
TG_CHAT_ID: "-4940243803"
|
||||||
|
# Go构建变量
|
||||||
|
SERVICE: ppanel
|
||||||
|
SERVICE_STYLE: ppanel
|
||||||
|
VERSION: ${{ github.sha }}
|
||||||
|
BUILDTIME: ${{ github.event.head_commit.timestamp }}
|
||||||
|
GOARCH: amd64
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ppanel-server
|
||||||
|
container:
|
||||||
|
image: node:20
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
# 只有node支持版本号别名
|
||||||
|
node: ['20.15.1']
|
||||||
|
steps:
|
||||||
|
# 步骤1: 下载代码
|
||||||
|
- name: 📥 下载代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# 步骤2: 设置动态环境变量
|
||||||
|
- name: ⚙️ 设置动态环境变量
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||||
|
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
|
||||||
|
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_PATH=/root/vpn_server" >> $GITHUB_ENV
|
||||||
|
echo "为 main 分支设置生产环境变量"
|
||||||
|
elif [ "${{ github.ref_name }}" = "dev" ]; then
|
||||||
|
echo "DOCKER_TAG_SUFFIX=dev" >> $GITHUB_ENV
|
||||||
|
echo "CONTAINER_NAME=ppanel-server-dev" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_PATH=/root/vpn_server_dev" >> $GITHUB_ENV
|
||||||
|
echo "为 dev 分支设置开发环境变量"
|
||||||
|
else
|
||||||
|
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_PATH=/root/vpn_server_other" >> $GITHUB_ENV
|
||||||
|
echo "为其他分支 (${{ github.ref_name }}) 设置环境变量"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 步骤3: 安装系统工具 (Docker, curl, jq)
|
||||||
|
- name: 🔧 安装系统工具 (Docker, curl, jq)
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
echo "等待 apt/dpkg 锁释放 (unattended-upgrades)..."
|
||||||
|
# 等待最多 300 秒让 unattended-upgrades/apt/dpkg 锁释放
|
||||||
|
end=$((SECONDS+300))
|
||||||
|
while true; do
|
||||||
|
LOCKS_BUSY=0
|
||||||
|
# 如果 unattended-upgrades 正在运行,标记为忙碌
|
||||||
|
if pgrep -x unattended-upgrades >/dev/null 2>&1; then LOCKS_BUSY=1; fi
|
||||||
|
# 如果 fuser 存在,检查常见的锁文件
|
||||||
|
if command -v fuser >/dev/null 2>&1; then
|
||||||
|
if fuser /var/lib/dpkg/lock >/dev/null 2>&1 \
|
||||||
|
|| fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 \
|
||||||
|
|| fuser /var/lib/apt/lists/lock >/dev/null 2>&1; then
|
||||||
|
LOCKS_BUSY=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# 如果不忙碌则跳出循环
|
||||||
|
if [ "$LOCKS_BUSY" -eq 0 ]; then break; fi
|
||||||
|
# 超时后约 5 分钟
|
||||||
|
if [ $SECONDS -ge $end ]; then
|
||||||
|
echo "等待 apt/dpkg 锁超时,使用 Dpkg::Lock::Timeout 继续..."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "仍在等待锁释放..."; sleep 5
|
||||||
|
done
|
||||||
|
apt-get update -y -o Dpkg::Lock::Timeout=600
|
||||||
|
apt-get install -y -o Dpkg::Lock::Timeout=600 jq curl ca-certificates docker.io
|
||||||
|
docker --version
|
||||||
|
jq --version
|
||||||
|
curl --version
|
||||||
|
|
||||||
|
# 步骤4: 构建并发布到镜像仓库
|
||||||
|
- name: 📤 构建并发布到镜像仓库
|
||||||
|
run: |
|
||||||
|
echo "开始构建并推送镜像..."
|
||||||
|
echo "仓库: ${{ env.REPO }}"
|
||||||
|
echo "版本标签: ${{ env.VERSION }}"
|
||||||
|
echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
|
|
||||||
|
# 构建镜像,同时打上版本和分支两个标签
|
||||||
|
docker build -f Dockerfile \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--build-arg TARGETARCH=amd64 \
|
||||||
|
--build-arg VERSION=${{ env.VERSION }} \
|
||||||
|
--build-arg BUILDTIME=${{ env.BUILDTIME }} \
|
||||||
|
-t ${{ env.REPO }}:${{ env.VERSION }} \
|
||||||
|
-t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }} \
|
||||||
|
.
|
||||||
|
|
||||||
|
echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}"
|
||||||
|
docker push ${{ env.REPO }}:${{ env.VERSION }}
|
||||||
|
|
||||||
|
echo "推送分支标签镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
|
docker push ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
|
||||||
|
|
||||||
|
echo "镜像推送完成"
|
||||||
|
|
||||||
|
# 步骤5: 连接服务器拉镜像启动
|
||||||
|
- name: 🚀 连接服务器拉镜像启动
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ env.SSH_HOST }}
|
||||||
|
username: ${{ env.SSH_USER }}
|
||||||
|
password: ${{ env.SSH_PASSWORD }}
|
||||||
|
port: ${{ env.SSH_PORT }}
|
||||||
|
timeout: 300s
|
||||||
|
command_timeout: 600s
|
||||||
|
script: |
|
||||||
|
echo "连接服务器成功,开始部署..."
|
||||||
|
echo "部署容器名: ${{ env.CONTAINER_NAME }}"
|
||||||
|
echo "部署路径: ${{ env.DEPLOY_PATH }}"
|
||||||
|
echo "部署镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
|
|
||||||
|
# 确保部署目录存在
|
||||||
|
mkdir -p ${{ env.DEPLOY_PATH }}/config
|
||||||
|
mkdir -p ${{ env.DEPLOY_PATH }}/logs
|
||||||
|
|
||||||
|
# 停止并删除旧容器(忽略所有错误)
|
||||||
|
if docker ps -a | grep -q ${{ env.CONTAINER_NAME }} 2>/dev/null; then
|
||||||
|
echo "停止旧容器..."
|
||||||
|
docker stop ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
|
||||||
|
echo "等待容器完全停止..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
echo "删除旧容器..."
|
||||||
|
# 静默删除,完全忽略错误输出
|
||||||
|
docker rm ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 如果仍然存在,尝试强制删除(静默)
|
||||||
|
if docker ps -a | grep -q ${{ env.CONTAINER_NAME }} 2>/dev/null; then
|
||||||
|
echo "尝试强制删除..."
|
||||||
|
docker rm -f ${{ env.CONTAINER_NAME }} >/dev/null 2>&1 || true
|
||||||
|
sleep 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "容器清理完成,继续部署..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 拉取最新分支镜像
|
||||||
|
echo "拉取镜像: ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}..."
|
||||||
|
docker pull ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
|
||||||
|
|
||||||
|
# 启动新容器
|
||||||
|
echo "启动新容器..."
|
||||||
|
cd ${{ env.DEPLOY_PATH }}
|
||||||
|
docker run -d \
|
||||||
|
--name ${{ env.CONTAINER_NAME }} \
|
||||||
|
--restart unless-stopped \
|
||||||
|
--network host \
|
||||||
|
-v ./config/ppanel.yaml:/app/etc/ppanel.yaml \
|
||||||
|
-v ./logs:/app/logs \
|
||||||
|
${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}
|
||||||
|
|
||||||
|
# 检查容器状态
|
||||||
|
sleep 5
|
||||||
|
if docker ps | grep -q ${{ env.CONTAINER_NAME }}; then
|
||||||
|
echo "✅ 容器启动成功"
|
||||||
|
else
|
||||||
|
echo "❌ 容器启动失败"
|
||||||
|
docker logs ${{ env.CONTAINER_NAME }}
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 步骤6: TG通知 (成功)
|
||||||
|
- name: 📱 发送成功通知到Telegram
|
||||||
|
if: success()
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
token: ${{ env.TG_BOT_TOKEN }}
|
||||||
|
to: ${{ env.TG_CHAT_ID }}
|
||||||
|
message: |
|
||||||
|
✅ 部署成功!
|
||||||
|
|
||||||
|
📦 项目: ${{ github.repository }}
|
||||||
|
🌿 分支: ${{ github.ref_name }}
|
||||||
|
📝 提交: ${{ github.sha }}
|
||||||
|
👤 提交者: ${{ github.actor }}
|
||||||
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
🚀 服务已成功部署到生产环境
|
||||||
|
parse_mode: Markdown
|
||||||
|
|
||||||
|
# 步骤5: TG通知 (失败)
|
||||||
|
- name: 📱 发送失败通知到Telegram
|
||||||
|
if: failure()
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
token: ${{ env.TG_BOT_TOKEN }}
|
||||||
|
to: ${{ env.TG_CHAT_ID }}
|
||||||
|
message: |
|
||||||
|
❌ 部署失败!
|
||||||
|
|
||||||
|
📦 项目: ${{ github.repository }}
|
||||||
|
🌿 分支: ${{ github.ref_name }}
|
||||||
|
📝 提交: ${{ github.sha }}
|
||||||
|
👤 提交者: ${{ github.actor }}
|
||||||
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
⚠️ 请检查构建日志获取详细信息
|
||||||
|
parse_mode: Markdown
|
||||||
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
name: Deploy
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["develop"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["develop"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Get short Git commit ID
|
|
||||||
id: vars
|
|
||||||
run: echo "COMMIT_ID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build Docker image
|
|
||||||
run: docker build -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 --restart=always -d ${{ secrets.DOCKER_USERNAME }}/ppanel-server-dev:${{ env.COMMIT_ID }}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
name: Release
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
IMAGE_NAME: ppanel-server
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract version from git tag
|
|
||||||
id: version
|
|
||||||
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Get short SHA
|
|
||||||
id: sha
|
|
||||||
run: echo "GIT_SHA=${GITHUB_SHA::8}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set BUILD_TIME env
|
|
||||||
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
|
|
||||||
|
|
||||||
|
|
||||||
- name: Build and push Docker image for main release
|
|
||||||
if: "!contains(github.ref_name, 'beta')"
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ env.VERSION }}
|
|
||||||
tags: |
|
|
||||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest
|
|
||||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image for beta release
|
|
||||||
if: contains(github.ref_name, 'beta')
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
build-args: |
|
|
||||||
VERSION=${{ env.VERSION }}
|
|
||||||
tags: |
|
|
||||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:beta
|
|
||||||
${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }}-${{ env.GIT_SHA }}
|
|
||||||
|
|
||||||
release-notes:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build-docker
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.21'
|
|
||||||
|
|
||||||
- name: Install GoReleaser
|
|
||||||
run: |
|
|
||||||
go install github.com/goreleaser/goreleaser/v2@latest
|
|
||||||
|
|
||||||
- name: Run GoReleaser
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
run: |
|
|
||||||
goreleaser check
|
|
||||||
goreleaser release --clean
|
|
||||||
|
|
||||||
releases-matrix:
|
|
||||||
name: Release ppanel-server binary
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release-notes # wait for release-notes job to finish
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
# build and publish in parallel: linux/386, linux/amd64, linux/arm64,
|
|
||||||
# windows/386, windows/amd64, windows/arm64, darwin/amd64, darwin/arm64
|
|
||||||
goos: [ linux, windows, darwin ]
|
|
||||||
goarch: [ '386', amd64, arm64 ]
|
|
||||||
exclude:
|
|
||||||
- goarch: '386'
|
|
||||||
goos: darwin
|
|
||||||
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Extract version from git tag
|
|
||||||
id: version
|
|
||||||
run: echo "VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Set BUILD_TIME env
|
|
||||||
run: echo BUILD_TIME=$(date --iso-8601=seconds) >> ${GITHUB_ENV}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: wangyoucao577/go-release-action@v1
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
goos: ${{ matrix.goos }}
|
|
||||||
goarch: ${{ matrix.goarch }}
|
|
||||||
asset_name: "ppanel-server-${{ matrix.goos }}-${{ matrix.goarch }}"
|
|
||||||
goversion: "https://dl.google.com/go/go1.23.3.linux-amd64.tar.gz"
|
|
||||||
project_path: "."
|
|
||||||
binary_name: "ppanel-server"
|
|
||||||
extra_files: LICENSE etc
|
|
||||||
ldflags: -X "github.com/perfect-panel/server/pkg/constant.Version=${{env.VERSION}}" -X "github.com/perfect-panel/server/pkg/constant.BuildTime=${{env.BUILD_TIME}}"
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
name: Go CI/CD with goctl and Swagger
|
|
||||||
|
|
||||||
on:
|
|
||||||
# release:
|
|
||||||
# types: [published]
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install goctl
|
|
||||||
run: |
|
|
||||||
curl -L https://github.com/zeromicro/go-zero/releases/download/tools%2Fgoctl%2Fv1.7.2/goctl-v1.7.2-linux-amd64.tar.gz -o goctl-v1.7.2-linux-amd64.tar.gz
|
|
||||||
tar -xvzf goctl-v1.7.2-linux-amd64.tar.gz
|
|
||||||
chmod +x goctl
|
|
||||||
sudo mv goctl /usr/local/bin/goctl
|
|
||||||
goctl --version
|
|
||||||
|
|
||||||
- name: Install goctl-swagger
|
|
||||||
run: |
|
|
||||||
curl -L https://github.com/tensionc/goctl-swagger/releases/download/v1.0.1/goctl-swagger-v1.0.1-linux-amd64.tar.gz -o goctl-swagger.tar.gz
|
|
||||||
tar -xvzf goctl-swagger.tar.gz
|
|
||||||
chmod +x goctl-swagger
|
|
||||||
sudo mv goctl-swagger /usr/local/bin/
|
|
||||||
|
|
||||||
- name: Generate Swagger file
|
|
||||||
run: |
|
|
||||||
mkdir -p swagger
|
|
||||||
goctl api plugin -plugin goctl-swagger='swagger -filename common.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_common.api -dir ./swagger
|
|
||||||
goctl api plugin -plugin goctl-swagger='swagger -filename user.json -pack Response -response "[{\"name\":\"code\",\"type\":\"integer\",\"description\":\"状态码\"},{\"name\":\"msg\",\"type\":\"string\",\"description\":\"消息\"},{\"name\":\"data\",\"type\":\"object\",\"description\":\"数据\",\"is_data\":true}]";' -api ./apis/swagger_user.api -dir ./swagger
|
|
||||||
goctl api plugin -plugin goctl-swagger='swagger -filename app.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_app.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/app.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
|
|
||||||
|
|
||||||
@@ -3,10 +3,8 @@
|
|||||||
*-dev.yaml
|
*-dev.yaml
|
||||||
*.local.yaml
|
*.local.yaml
|
||||||
/test/
|
/test/
|
||||||
*.log
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*_test_config.go
|
*_test_config.go
|
||||||
*.log*
|
|
||||||
/build/
|
/build/
|
||||||
*.p8
|
*.p8
|
||||||
*.crt
|
*.crt
|
||||||
|
|||||||
@@ -1,15 +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" />
|
|
||||||
<envs>
|
|
||||||
<env name="PPANEL_MODE" value="demo" />
|
|
||||||
</envs>
|
|
||||||
<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>
|
|
||||||
+4
-2
@@ -24,12 +24,13 @@ RUN BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") && \
|
|||||||
go build -ldflags="-s -w -X 'github.com/perfect-panel/server/pkg/constant.Version=${VERSION}' -X 'github.com/perfect-panel/server/pkg/constant.BuildTime=${BUILD_TIME}'" -o /app/ppanel ppanel.go
|
go build -ldflags="-s -w -X 'github.com/perfect-panel/server/pkg/constant.Version=${VERSION}' -X 'github.com/perfect-panel/server/pkg/constant.BuildTime=${BUILD_TIME}'" -o /app/ppanel ppanel.go
|
||||||
|
|
||||||
# Final minimal image
|
# Final minimal image
|
||||||
FROM scratch
|
FROM alpine:latest
|
||||||
|
|
||||||
# Copy CA certificates and timezone data
|
# Copy CA certificates and timezone data
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
|
||||||
|
# Set Shanghai timezone
|
||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
# Set working directory and copy binary
|
# Set working directory and copy binary
|
||||||
@@ -37,6 +38,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY --from=builder /app/ppanel /app/ppanel
|
COPY --from=builder /app/ppanel /app/ppanel
|
||||||
COPY --from=builder /build/etc /app/etc
|
COPY --from=builder /build/etc /app/etc
|
||||||
|
COPY --from=builder /build/logs /app/logs
|
||||||
|
|
||||||
# Expose the port (optional)
|
# Expose the port (optional)
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
@@ -1,84 +1,73 @@
|
|||||||
NAME="ppanel-server"
|
# Custom configuration | 独立配置
|
||||||
BINDIR=bin
|
# Service name | 项目名称
|
||||||
VERSION=$(shell git describe --tags || echo "unknown version")
|
SERVICE=ppanel
|
||||||
BUILDTIME=$(shell date -u)
|
# Service name in specific style | 项目经过style格式化的名称
|
||||||
|
SERVICE_STYLE=ppanel
|
||||||
|
|
||||||
|
# Service name in lowercase | 项目名称全小写格式
|
||||||
|
SERVICE_LOWER=ppanel
|
||||||
|
# Service name in snake format | 项目名称下划线格式
|
||||||
|
SERVICE_SNAKE=ppanel
|
||||||
|
# Service name in snake format | 项目名称短杠格式
|
||||||
|
SERVICE_DASH=ppanel
|
||||||
|
|
||||||
|
# The project version, if you don't use git, you should set it manually | 项目版本,如果不使用git请手动设置
|
||||||
|
VERSION=$(shell git describe --tags --always)
|
||||||
|
# Build time | 构建时间
|
||||||
|
BUILDTIME=$(shell date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
# The project file name style | 项目文件命名风格
|
||||||
|
PROJECT_STYLE=go_zero
|
||||||
|
|
||||||
|
# Whether to use i18n | 是否启用 i18n
|
||||||
|
PROJECT_I18N=true
|
||||||
|
|
||||||
|
# The suffix after build or compile | 构建后缀
|
||||||
|
PROJECT_BUILD_SUFFIX=api
|
||||||
|
|
||||||
|
|
||||||
|
# The arch of the build | 构建的架构
|
||||||
|
GOARCH=amd64
|
||||||
|
|
||||||
|
# The repository of docker | Docker 仓库地址
|
||||||
|
DOCKER_REPO=docker.i.on ticcheck
|
||||||
|
|
||||||
|
# ---- You may not need to modify the codes below | 下面的代码大概率不需要更改 ----
|
||||||
|
|
||||||
|
GO ?= go
|
||||||
|
GOFMT ?= gofmt "-s"
|
||||||
|
GOFILES := $(shell find . -name "*.go")
|
||||||
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
|
# Build command with version injection | 带版本信息注入的构建命令
|
||||||
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/perfect-panel/server/pkg/constant.Version=$(VERSION)" \
|
GOBUILD=CGO_ENABLED=0 go build -trimpath -ldflags '-X "github.com/perfect-panel/server/pkg/constant.Version=$(VERSION)" \
|
||||||
-X "github.com/perfect-panel/server/pkg/constant.BuildTime=$(BUILDTIME)" \
|
-X "github.com/perfect-panel/server/pkg/constant.BuildTime=$(BUILDTIME)" \
|
||||||
-w -s -buildid='
|
-w -s -buildid='
|
||||||
|
|
||||||
PLATFORM_LIST = \
|
.PHONY: docker
|
||||||
darwin-amd64 \
|
docker: # Build the docker image | 构建 docker 镜像
|
||||||
darwin-amd64-v3 \
|
docker build -f Dockerfile -t ${API_IMAGE_NAME}:${VERSION} .
|
||||||
darwin-arm64 \
|
@echo "Build docker successfully"
|
||||||
linux-386 \
|
|
||||||
linux-amd64 \
|
|
||||||
linux-amd64-v3 \
|
|
||||||
linux-armv5 \
|
|
||||||
linux-armv6 \
|
|
||||||
linux-armv7 \
|
|
||||||
linux-arm64 \
|
|
||||||
|
|
||||||
WINDOWS_ARCH_LIST = \
|
.PHONY: publish-docker
|
||||||
windows-386 \
|
publish-docker: # Publish docker image | 发布 docker 镜像
|
||||||
windows-amd64 \
|
docker tag ${API_IMAGE_NAME}:${VERSION} ${API_IMAGE_NAME}:latest
|
||||||
windows-amd64-v3 \
|
docker push ${API_IMAGE_NAME}:latest
|
||||||
windows-arm64 \
|
@echo "Publish docker successfully"
|
||||||
windows-armv7
|
|
||||||
|
|
||||||
all: linux-amd64 darwin-amd64 windows-amd64 # Most used
|
.PHONY: docker-run
|
||||||
|
docker-run: # Publish docker image | 发布 docker 镜像
|
||||||
darwin-amd64:
|
docker rm -f ${API_CONTAINER_NAME}
|
||||||
GOARCH=amd64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
@echo "pwd: $(pwd)"
|
||||||
|
docker compose -p ${DOCKER_PROJECT_NAME} -f ${DOCKER_COMPOSE_FILE} up -d
|
||||||
darwin-amd64-v3:
|
@echo "docker run successfully"
|
||||||
GOARCH=amd64 GOOS=darwin GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
darwin-arm64:
|
|
||||||
GOARCH=arm64 GOOS=darwin $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-386:
|
|
||||||
GOARCH=386 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-amd64:
|
|
||||||
GOARCH=amd64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-amd64-v3:
|
|
||||||
GOARCH=amd64 GOOS=linux GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-armv5:
|
|
||||||
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-armv6:
|
|
||||||
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-armv7:
|
|
||||||
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
linux-arm64:
|
|
||||||
GOARCH=arm64 GOOS=linux $(GOBUILD) -o $(BINDIR)/$(NAME)-$@
|
|
||||||
|
|
||||||
windows-386:
|
|
||||||
GOARCH=386 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
|
||||||
|
|
||||||
windows-amd64:
|
|
||||||
GOARCH=amd64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
|
||||||
|
|
||||||
windows-amd64-v3:
|
|
||||||
GOARCH=amd64 GOOS=windows GOAMD64=v3 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
|
||||||
|
|
||||||
windows-arm64:
|
|
||||||
GOARCH=arm64 GOOS=windows $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
|
||||||
|
|
||||||
windows-armv7:
|
|
||||||
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@.exe
|
|
||||||
|
|
||||||
|
|
||||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
.PHONY: build-linux
|
||||||
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
build-linux: # Build project for Linux | 构建Linux下的可执行文件
|
||||||
|
GOOS=linux GOARCH=$(GOARCH) $(GOBUILD) -o /app/ppanel $(SERVICE_STYLE).go
|
||||||
|
@echo "Build project for Linux successfully"
|
||||||
|
|
||||||
$(gz_releases): %.gz : %
|
.PHONY: help
|
||||||
chmod +x $(BINDIR)/$(NAME)-$(basename $@)
|
help: # Show help | 显示帮助
|
||||||
gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
|
@grep kfile | sort | while read-r l; do printf "\033[1;32m$$(echo $$l | cut -f 1 -d':')\033[00m:$$(echo $$l | cut -f 2- -d'#')\n"; done
|
||||||
|
|
||||||
$(zip_releases): %.zip : %
|
|
||||||
zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ proxy services. Built with Go, it emphasizes performance, security, and scalabil
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. **Pull from Docker Hub** (after CI/CD publishes):
|
4. **Pull from Docker Hub** (after CI/CD publishes):
|
||||||
```bash
|
```bashw
|
||||||
docker pull yourusername/ppanel-server:latest
|
docker pull yourusername/ppanel-server:latest
|
||||||
docker run --rm -p 8080:8080 yourusername/ppanel-server:latest
|
docker run --rm -p 8080:8080 yourusername/ppanel-server:latest
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ type (
|
|||||||
Status *uint8 `json:"status" validate:"required"`
|
Status *uint8 `json:"status" validate:"required"`
|
||||||
}
|
}
|
||||||
GetTicketListRequest {
|
GetTicketListRequest {
|
||||||
Page int64 `form:"page"`
|
Page int64 `form:"page"`
|
||||||
Size int64 `form:"size"`
|
Size int64 `form:"size"`
|
||||||
UserId int64 `form:"user_id,omitempty"`
|
UserId int64 `form:"user_id,omitempty"`
|
||||||
Status *uint8 `form:"status,omitempty"`
|
Status *uint8 `form:"status,omitempty"`
|
||||||
Search string `form:"search,omitempty"`
|
IssueType *uint8 `form:"issue_type,omitempty"`
|
||||||
|
Search string `form:"search,omitempty"`
|
||||||
}
|
}
|
||||||
GetTicketListResponse {
|
GetTicketListResponse {
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type (
|
|||||||
Telegram int64 `json:"telegram"`
|
Telegram int64 `json:"telegram"`
|
||||||
ReferCode string `json:"refer_code"`
|
ReferCode string `json:"refer_code"`
|
||||||
RefererId int64 `json:"referer_id"`
|
RefererId int64 `json:"referer_id"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ type (
|
|||||||
CreateUserTicketRequest {
|
CreateUserTicketRequest {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
IssueType uint8 `json:"issue_type"`
|
||||||
}
|
}
|
||||||
GetUserTicketListRequest {
|
GetUserTicketListRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
Status *uint8 `form:"status,omitempty"`
|
Status *uint8 `form:"status,omitempty"`
|
||||||
Search string `form:"search,omitempty"`
|
IssueType *uint8 `form:"issue_type,omitempty"`
|
||||||
|
Search string `form:"search,omitempty"`
|
||||||
}
|
}
|
||||||
GetUserTicketDetailRequest {
|
GetUserTicketDetailRequest {
|
||||||
Id int64 `form:"id" validate:"required"`
|
Id int64 `form:"id" validate:"required"`
|
||||||
|
|||||||
+16
-4
@@ -2,11 +2,11 @@ Host: 0.0.0.0
|
|||||||
Port: 8080
|
Port: 8080
|
||||||
Debug: false
|
Debug: false
|
||||||
JwtAuth:
|
JwtAuth:
|
||||||
AccessSecret: 1234567890
|
AccessSecret: e4bbdb8ca57179374c0e8a7a85038da21341aee4bdff98c0e612751d7c362cac
|
||||||
AccessExpire: 604800
|
AccessExpire: 604800
|
||||||
Logger:
|
Logger:
|
||||||
ServiceName: PPanel
|
ServiceName: PPanel
|
||||||
Mode: console
|
Mode: file
|
||||||
Encoding: plain
|
Encoding: plain
|
||||||
TimeFormat: '2025-01-01 00:00:00.000'
|
TimeFormat: '2025-01-01 00:00:00.000'
|
||||||
Path: logs
|
Path: logs
|
||||||
@@ -21,7 +21,7 @@ Logger:
|
|||||||
Rotation: daily
|
Rotation: daily
|
||||||
FileTimeFormat: 2025-01-01T00:00:00.000Z00:00
|
FileTimeFormat: 2025-01-01T00:00:00.000Z00:00
|
||||||
MySQL:
|
MySQL:
|
||||||
Addr: 172.245.180.199:3306
|
Addr: ppanel-db:3306
|
||||||
Dbname: ppanel
|
Dbname: ppanel
|
||||||
Username: ppanel
|
Username: ppanel
|
||||||
Password: ppanelpassword
|
Password: ppanelpassword
|
||||||
@@ -35,4 +35,16 @@ Redis:
|
|||||||
DB: 0
|
DB: 0
|
||||||
Administrator:
|
Administrator:
|
||||||
Password: password
|
Password: password
|
||||||
Email: admin@ppanel.dev
|
Email: admin@ppanel.dev
|
||||||
|
Email:
|
||||||
|
Enable: true
|
||||||
|
Platform: smtp
|
||||||
|
PlatformConfig: '{"host":"smtp.gmail.com","port":465,"user":"devneeds52@gmail.com","pass":"bqxn hurw ckxt ookj","from":"devneeds52@gmail.com","ssl":true}'
|
||||||
|
EnableVerify: true
|
||||||
|
EnableNotify: true
|
||||||
|
EnableDomainSuffix: false
|
||||||
|
DomainSuffixList: ""
|
||||||
|
VerifyEmailTemplate: ""
|
||||||
|
ExpirationEmailTemplate: ""
|
||||||
|
MaintenanceEmailTemplate: ""
|
||||||
|
TrafficExceedEmailTemplate: ""
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -382,6 +382,7 @@ CREATE TABLE IF NOT EXISTS `user`
|
|||||||
`referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID',
|
`referer_id` bigint DEFAULT NULL COMMENT 'Referrer ID',
|
||||||
`commission` bigint DEFAULT '0' COMMENT 'Commission',
|
`commission` bigint DEFAULT '0' COMMENT 'Commission',
|
||||||
`gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount',
|
`gift_amount` bigint DEFAULT '0' COMMENT 'User Gift Amount',
|
||||||
|
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT 'User Remark',
|
||||||
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled',
|
`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT 'Is Account Enabled',
|
||||||
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin',
|
`is_admin` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Admin',
|
||||||
`valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified',
|
`valid_email` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'Is Email Verified',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/perfect-panel/server/internal/logic/public/user"
|
"github.com/perfect-panel/server/internal/logic/public/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/result"
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,8 +12,15 @@ import (
|
|||||||
func QueryUserBalanceLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func QueryUserBalanceLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
|
||||||
|
var req types.QueryUserBalanceLogListRequest
|
||||||
|
_ = c.ShouldBind(&req)
|
||||||
|
validateErr := svcCtx.Validate(&req)
|
||||||
|
if validateErr != nil {
|
||||||
|
result.ParamErrorResult(c, validateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
l := user.NewQueryUserBalanceLogLogic(c.Request.Context(), svcCtx)
|
l := user.NewQueryUserBalanceLogLogic(c.Request.Context(), svcCtx)
|
||||||
resp, err := l.QueryUserBalanceLog()
|
resp, err := l.QueryUserBalanceLog(&req)
|
||||||
result.HttpResult(c, resp, err)
|
result.HttpResult(c, resp, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ func (l *GetSubscribeListLogic) GetSubscribeList(req *types.GetSubscribeListRequ
|
|||||||
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount))
|
l.Logger.Error("[GetSubscribeListLogic] JSON unmarshal failed: ", logger.Field("error", err.Error()), logger.Field("discount", item.Discount))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, sub.ServerGroup)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||||
|
sub.ServerCount = 0
|
||||||
|
} else {
|
||||||
|
sub.ServerCount = int64(len(servers))
|
||||||
|
}
|
||||||
sub.Server = tool.StringToInt64Slice(item.Server)
|
sub.Server = tool.StringToInt64Slice(item.Server)
|
||||||
sub.ServerGroup = tool.StringToInt64Slice(item.ServerGroup)
|
sub.ServerGroup = tool.StringToInt64Slice(item.ServerGroup)
|
||||||
resultList = append(resultList, sub)
|
resultList = append(resultList, sub)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func NewGetTicketListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *GetTicketListLogic) GetTicketList(req *types.GetTicketListRequest) (resp *types.GetTicketListResponse, err error) {
|
func (l *GetTicketListLogic) GetTicketList(req *types.GetTicketListRequest) (resp *types.GetTicketListResponse, err error) {
|
||||||
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, int(req.Page), int(req.Size), req.UserId, req.Status, req.Search)
|
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, int(req.Page), int(req.Size), req.UserId, req.Status, req.IssueType, req.Search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[GetTicketList] Query Database Error: ", logger.Field("error", err.Error()))
|
l.Errorw("[GetTicketList] Query Database Error: ", logger.Field("error", err.Error()))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err)
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
userInfo.Balance = req.Balance
|
userInfo.Balance = req.Balance
|
||||||
userInfo.GiftAmount = req.GiftAmount
|
userInfo.GiftAmount = req.GiftAmount
|
||||||
userInfo.Commission = req.Commission
|
userInfo.Commission = req.Commission
|
||||||
|
userInfo.Remark = req.Remark
|
||||||
// 手动设置 IsAdmin 字段,因为类型不匹配(*bool vs bool)
|
// 手动设置 IsAdmin 字段,因为类型不匹配(*bool vs bool)
|
||||||
userInfo.IsAdmin = &req.IsAdmin
|
userInfo.IsAdmin = &req.IsAdmin
|
||||||
userInfo.Enable = &req.Enable
|
userInfo.Enable = &req.Enable
|
||||||
|
|||||||
@@ -3,11 +3,6 @@ package common
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
@@ -68,52 +63,14 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
|
|||||||
l.Logger.Error("[GetStatLogic] get server_addr failed: ", logger.Field("error", err.Error()))
|
l.Logger.Error("[GetStatLogic] get server_addr failed: ", logger.Field("error", err.Error()))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server_addr failed: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get server_addr failed: %v", err.Error())
|
||||||
}
|
}
|
||||||
type apireq struct {
|
|
||||||
query string
|
country := 0
|
||||||
fields string
|
|
||||||
}
|
protocolDict := make(map[string]struct{})
|
||||||
type apiret struct {
|
|
||||||
CountryCode string `json:"countryCode"`
|
|
||||||
}
|
|
||||||
//map as dict
|
|
||||||
type void struct{}
|
|
||||||
var v void
|
|
||||||
country := make(map[string]void)
|
|
||||||
for c := range slices.Chunk(nodeaddr, 100) {
|
|
||||||
var batchreq []apireq
|
|
||||||
for _, addr := range c {
|
|
||||||
isAddr := net.ParseIP(addr)
|
|
||||||
if isAddr == nil {
|
|
||||||
ip, err := net.LookupIP(addr)
|
|
||||||
if err == nil && len(ip) > 0 {
|
|
||||||
batchreq = append(batchreq, apireq{query: ip[0].String(), fields: "countryCode"})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
batchreq = append(batchreq, apireq{query: addr, fields: "countryCode"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req, _ := json.Marshal(batchreq)
|
|
||||||
ret, err := http.Post("http://ip-api.com/batch", "application/json", strings.NewReader(string(req)))
|
|
||||||
if err == nil {
|
|
||||||
retBytes, err := io.ReadAll(ret.Body)
|
|
||||||
if err == nil {
|
|
||||||
var retStruct []apiret
|
|
||||||
err := json.Unmarshal(retBytes, &retStruct)
|
|
||||||
if err == nil {
|
|
||||||
for _, dat := range retStruct {
|
|
||||||
if dat.CountryCode != "" {
|
|
||||||
country[dat.CountryCode] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
protocolDict := make(map[string]void)
|
|
||||||
var protocol []string
|
var protocol []string
|
||||||
l.svcCtx.DB.Model(&server.Server{}).Where("enable = true").Pluck("protocol", &protocol)
|
l.svcCtx.DB.Model(&server.Server{}).Where("enable = true").Pluck("protocol", &protocol)
|
||||||
for _, p := range protocol {
|
for _, p := range protocol {
|
||||||
protocolDict[p] = v
|
protocolDict[p] = struct{}{}
|
||||||
}
|
}
|
||||||
protocol = nil
|
protocol = nil
|
||||||
for p := range protocolDict {
|
for p := range protocolDict {
|
||||||
@@ -122,7 +79,7 @@ func (l *GetStatLogic) GetStat() (resp *types.GetStatResponse, err error) {
|
|||||||
resp = &types.GetStatResponse{
|
resp = &types.GetStatResponse{
|
||||||
User: u,
|
User: u,
|
||||||
Node: n,
|
Node: n,
|
||||||
Country: int64(len(country)),
|
Country: int64(country),
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
OnlineDevice: l.svcCtx.DeviceManager.GetOnlineDeviceCount(),
|
OnlineDevice: l.svcCtx.DeviceManager.GetOnlineDeviceCount(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
|||||||
// Generate verification code
|
// Generate verification code
|
||||||
code := random.Key(6, 0)
|
code := random.Key(6, 0)
|
||||||
taskPayload.Email = req.Email
|
taskPayload.Email = req.Email
|
||||||
taskPayload.Subject = "Verification code"
|
taskPayload.Subject = "Verification Code"
|
||||||
content, err := l.initTemplate(req.Type, code)
|
content, err := l.initTemplate(req.Type, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Logger.Error("[SendEmailCode]: InitTemplate Error", logger.Field("error", err.Error()))
|
l.Logger.Error("[SendEmailCode]: InitTemplate Error", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@@ -50,12 +50,17 @@ func (l *QuerySubscribeListLogic) QuerySubscribeList() (resp *types.QuerySubscri
|
|||||||
}
|
}
|
||||||
list[i] = sub
|
list[i] = sub
|
||||||
// 通过服务组查询关联的节点数量
|
// 通过服务组查询关联的节点数量
|
||||||
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, sub.ServerGroup)
|
if item.ServerGroup != "" {
|
||||||
if err != nil {
|
groupIds := tool.StringToInt64Slice(item.ServerGroup)
|
||||||
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
servers, err := l.svcCtx.ServerModel.FindServerListByGroupIds(l.ctx, groupIds)
|
||||||
sub.ServerCount = 0
|
if err != nil {
|
||||||
|
l.Errorw("[QuerySubscribeListLogic] FindServerListByGroupIds error", logger.Field("error", err.Error()))
|
||||||
|
sub.ServerCount = 0
|
||||||
|
} else {
|
||||||
|
sub.ServerCount = int64(len(servers))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sub.ServerCount = int64(len(servers))
|
sub.ServerCount = 0
|
||||||
}
|
}
|
||||||
list[i] = sub
|
list[i] = sub
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ func (l *CreateUserTicketLogic) CreateUserTicket(req *types.CreateUserTicketRequ
|
|||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
UserId: u.Id,
|
UserId: u.Id,
|
||||||
Status: ticket.Pending,
|
Status: ticket.Pending,
|
||||||
|
IssueType: req.IssueType,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert ticket error: %v", err.Error())
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert ticket error: %v", err.Error())
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (l *GetUserTicketListLogic) GetUserTicketList(req *types.GetUserTicketListR
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
l.Logger.Debugf("Current user: %v", u.Id)
|
l.Logger.Debugf("Current user: %v", u.Id)
|
||||||
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, req.Page, req.Size, u.Id, req.Status, req.Search)
|
total, list, err := l.svcCtx.TicketModel.QueryTicketList(l.ctx, req.Page, req.Size, u.Id, req.Status, req.IssueType, req.Search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("[GetUserTicketListLogic] Database Error", logger.Field("error", err.Error()))
|
l.Errorw("[GetUserTicketListLogic] Database Error", logger.Field("error", err.Error()))
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "QueryTicketList error: %v", err)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func NewQueryUserBalanceLogLogic(ctx context.Context, svcCtx *svc.ServiceContext
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog() (resp *types.QueryUserBalanceLogListResponse, err error) {
|
func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog(req *types.QueryUserBalanceLogListRequest) (resp *types.QueryUserBalanceLogListResponse, err error) {
|
||||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
logger.Error("current user is not found in context")
|
logger.Error("current user is not found in context")
|
||||||
@@ -41,7 +41,7 @@ func (l *QueryUserBalanceLogLogic) QueryUserBalanceLog() (resp *types.QueryUserB
|
|||||||
var total int64
|
var total int64
|
||||||
// Query User Balance Log
|
// Query User Balance Log
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
err = l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||||
return db.Model(&user.BalanceLog{}).Order("created_at DESC").Where("user_id = ?", u.Id).Count(&total).Find(&data).Error
|
return db.Model(&user.BalanceLog{}).Order("created_at DESC").Where("user_id = ?", u.Id).Count(&total).Offset((req.Page - 1) * req.Size).Limit(req.Size).Find(&data).Error
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log failed: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query User Balance Log failed: %v", err)
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RelayModeNone = "none"
|
RelayModeNone = "none"
|
||||||
RelayModeAll = "all"
|
RelayModeAll = "all"
|
||||||
RelayModeRandom = "random"
|
RelayModeRandom = "random"
|
||||||
RuleGroupTypeReject = "reject"
|
RelayModeMixed = "mixed"
|
||||||
RuleGroupTypeDefault = "default"
|
RelayModeAllDirect = "all_direct"
|
||||||
RuleGroupTypeDirect = "direct"
|
RelayModeRandomDirect = "random_direct"
|
||||||
|
RuleGroupTypeReject = "reject"
|
||||||
|
RuleGroupTypeDefault = "default"
|
||||||
|
RuleGroupTypeDirect = "direct"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ServerFilter struct {
|
type ServerFilter struct {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func (m *customSubscribeModel) QuerySubscribeListByShow(ctx context.Context) ([]
|
|||||||
var list []*Subscribe
|
var list []*Subscribe
|
||||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||||
conn = conn.Model(&Subscribe{})
|
conn = conn.Model(&Subscribe{})
|
||||||
return conn.Where("`show` = true").Find(v).Error
|
return conn.Where("`show` = true").Order("sort ASC").Find(v).Error
|
||||||
})
|
})
|
||||||
return list, err
|
return list, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Details struct {
|
|||||||
Description string `gorm:"type:text;comment:Description"`
|
Description string `gorm:"type:text;comment:Description"`
|
||||||
UserId int64 `gorm:"type:bigint;not null;default:0;comment:UserId"`
|
UserId int64 `gorm:"type:bigint;not null;default:0;comment:UserId"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status"`
|
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status"`
|
||||||
|
IssueType uint8 `gorm:"type:tinyint(1);not null;default:0;comment:Issue Type: 0 ticket, 1 withdraw"`
|
||||||
Follows []Follow `gorm:"foreignKey:TicketId;references:Id"`
|
Follows []Follow `gorm:"foreignKey:TicketId;references:Id"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
@@ -24,7 +25,7 @@ type Details struct {
|
|||||||
type customTicketLogicModel interface {
|
type customTicketLogicModel interface {
|
||||||
QueryTicketDetail(ctx context.Context, id int64) (*Details, error)
|
QueryTicketDetail(ctx context.Context, id int64) (*Details, error)
|
||||||
InsertTicketFollow(ctx context.Context, data *Follow) error
|
InsertTicketFollow(ctx context.Context, data *Follow) error
|
||||||
QueryTicketList(ctx context.Context, page, size int, userId int64, status *uint8, search string) (int64, []*Ticket, error)
|
QueryTicketList(ctx context.Context, page, size int, userId int64, status *uint8, issueType *uint8, search string) (int64, []*Ticket, error)
|
||||||
UpdateTicketStatus(ctx context.Context, id, userId int64, status uint8) error
|
UpdateTicketStatus(ctx context.Context, id, userId int64, status uint8) error
|
||||||
QueryWaitReplyTotal(ctx context.Context) (int64, error)
|
QueryWaitReplyTotal(ctx context.Context) (int64, error)
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ func (m *customTicketModel) InsertTicketFollow(ctx context.Context, data *Follow
|
|||||||
}
|
}
|
||||||
|
|
||||||
// QueryTicketList returns the ticket list.
|
// QueryTicketList returns the ticket list.
|
||||||
func (m *customTicketModel) QueryTicketList(ctx context.Context, page, size int, userId int64, status *uint8, search string) (int64, []*Ticket, error) {
|
func (m *customTicketModel) QueryTicketList(ctx context.Context, page, size int, userId int64, status *uint8, issueType *uint8, search string) (int64, []*Ticket, error) {
|
||||||
var data []*Ticket
|
var data []*Ticket
|
||||||
var total int64
|
var total int64
|
||||||
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, &data, func(conn *gorm.DB, v interface{}) error {
|
||||||
@@ -65,8 +66,9 @@ func (m *customTicketModel) QueryTicketList(ctx context.Context, page, size int,
|
|||||||
}
|
}
|
||||||
if status != nil {
|
if status != nil {
|
||||||
query = query.Where("status = ?", status)
|
query = query.Where("status = ?", status)
|
||||||
} else {
|
}
|
||||||
query = query.Where("status != ?", 4)
|
if issueType != nil {
|
||||||
|
query = query.Where("issue_type = ?", issueType)
|
||||||
}
|
}
|
||||||
if search != "" {
|
if search != "" {
|
||||||
query = query.Where("title like ? or description like ?", "%"+search+"%", "%"+search+"%")
|
query = query.Where("title like ? or description like ?", "%"+search+"%", "%"+search+"%")
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ const (
|
|||||||
type Ticket struct {
|
type Ticket struct {
|
||||||
Id int64 `gorm:"primaryKey"`
|
Id int64 `gorm:"primaryKey"`
|
||||||
Title string `gorm:"type:varchar(255);not null;default:'';comment:Title"`
|
Title string `gorm:"type:varchar(255);not null;default:'';comment:Title"`
|
||||||
Description string `gorm:"type:text;comment:Description"`
|
Description string `gorm:"type:longtext;comment:Description"`
|
||||||
UserId int64 `gorm:"type:bigint;not null;default:0;comment:UserId"`
|
UserId int64 `gorm:"type:bigint;not null;default:0;comment:UserId"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status"`
|
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status"`
|
||||||
|
IssueType uint8 `gorm:"type:tinyint(1);not null;default:0;comment:Issue Type: 0 ticket, 1 withdraw"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,6 @@ func (m *customTrafficModel) TopUsersTrafficByMonthly(ctx context.Context, date
|
|||||||
func (m *customTrafficModel) QueryTrafficLogPageList(ctx context.Context, userId, subscribeId int64, page, size int) ([]*TrafficLog, int64, error) {
|
func (m *customTrafficModel) QueryTrafficLogPageList(ctx context.Context, userId, subscribeId int64, page, size int) ([]*TrafficLog, int64, error) {
|
||||||
var list []*TrafficLog
|
var list []*TrafficLog
|
||||||
var total int64
|
var total int64
|
||||||
err := m.Conn.WithContext(ctx).Model(&TrafficLog{}).Where("user_id = ? and subscribe_id= ?", userId, subscribeId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error
|
err := m.Conn.WithContext(ctx).Model(&TrafficLog{}).Where("user_id = ? and subscribe_id= ?", userId, subscribeId).Count(&total).Order("timestamp DESC").Limit(size).Offset((page - 1) * size).Find(&list).Error
|
||||||
return list, total, err
|
return list, total, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type User struct {
|
|||||||
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
|
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
|
||||||
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
|
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
|
||||||
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
|
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
|
||||||
|
Remark string `gorm:"type:varchar(255);default:'';comment:User Remark"`
|
||||||
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
|
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
|
||||||
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
|
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
|
||||||
EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"`
|
EnableBalanceNotify *bool `gorm:"default:false;not null;comment:Enable Balance Change Notifications"`
|
||||||
@@ -45,6 +46,7 @@ type OldUser struct {
|
|||||||
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
|
RefererId int64 `gorm:"index:idx_referer;comment:Referrer ID"`
|
||||||
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
|
Commission int64 `gorm:"default:0;comment:Commission"` // Commission Amount
|
||||||
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
|
GiftAmount int64 `gorm:"default:0;comment:User Gift Amount"`
|
||||||
|
Remark string `gorm:"type:varchar(255);default:'';comment:User Remark"`
|
||||||
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
|
Enable *bool `gorm:"default:true;not null;comment:Is Account Enabled"`
|
||||||
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
|
IsAdmin *bool `gorm:"default:false;not null;comment:Is Admin"`
|
||||||
ValidEmail *bool `gorm:"default:false;not null;comment:Is Email Verified"`
|
ValidEmail *bool `gorm:"default:false;not null;comment:Is Email Verified"`
|
||||||
|
|||||||
+20
-9
@@ -529,6 +529,7 @@ type CreateUserTicketFollowRequest struct {
|
|||||||
type CreateUserTicketRequest struct {
|
type CreateUserTicketRequest struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
IssueType uint8 `json:"issue_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Currency struct {
|
type Currency struct {
|
||||||
@@ -905,11 +906,12 @@ type GetSubscriptionResponse struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetTicketListRequest struct {
|
type GetTicketListRequest struct {
|
||||||
Page int64 `form:"page"`
|
Page int64 `form:"page"`
|
||||||
Size int64 `form:"size"`
|
Size int64 `form:"size"`
|
||||||
UserId int64 `form:"user_id,omitempty"`
|
UserId int64 `form:"user_id,omitempty"`
|
||||||
Status *uint8 `form:"status,omitempty"`
|
Status *uint8 `form:"status,omitempty"`
|
||||||
Search string `form:"search,omitempty"`
|
IssueType *uint8 `form:"issue_type,omitempty"`
|
||||||
|
Search string `form:"search,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetTicketListResponse struct {
|
type GetTicketListResponse struct {
|
||||||
@@ -1021,10 +1023,11 @@ type GetUserTicketDetailRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GetUserTicketListRequest struct {
|
type GetUserTicketListRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
Status *uint8 `form:"status,omitempty"`
|
Status *uint8 `form:"status,omitempty"`
|
||||||
Search string `form:"search,omitempty"`
|
IssueType *uint8 `form:"issue_type,omitempty"`
|
||||||
|
Search string `form:"search,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUserTicketListResponse struct {
|
type GetUserTicketListResponse struct {
|
||||||
@@ -1391,6 +1394,11 @@ type QueryUserAffiliateListRequest struct {
|
|||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryUserBalanceLogListRequest struct {
|
||||||
|
Page int `form:"page"`
|
||||||
|
Size int `form:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
type QueryUserAffiliateListResponse struct {
|
type QueryUserAffiliateListResponse struct {
|
||||||
List []UserAffiliate `json:"list"`
|
List []UserAffiliate `json:"list"`
|
||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
@@ -1764,6 +1772,7 @@ type Ticket struct {
|
|||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
Follows []Follow `json:"follow,omitempty"`
|
Follows []Follow `json:"follow,omitempty"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
|
IssueType uint8 `json:"issue_type"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -2030,6 +2039,7 @@ type UpdateUserBasiceInfoRequest struct {
|
|||||||
Telegram int64 `json:"telegram"`
|
Telegram int64 `json:"telegram"`
|
||||||
ReferCode string `json:"refer_code"`
|
ReferCode string `json:"refer_code"`
|
||||||
RefererId int64 `json:"referer_id"`
|
RefererId int64 `json:"referer_id"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
}
|
}
|
||||||
@@ -2076,6 +2086,7 @@ type User struct {
|
|||||||
Telegram int64 `json:"telegram"`
|
Telegram int64 `json:"telegram"`
|
||||||
ReferCode string `json:"refer_code"`
|
ReferCode string `json:"refer_code"`
|
||||||
RefererId int64 `json:"referer_id"`
|
RefererId int64 `json:"referer_id"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
IsAdmin bool `json:"is_admin,omitempty"`
|
IsAdmin bool `json:"is_admin,omitempty"`
|
||||||
EnableBalanceNotify bool `json:"enable_balance_notify"`
|
EnableBalanceNotify bool `json:"enable_balance_notify"`
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ func AnyTLSUri(data proxy.Proxy, uuid string) string {
|
|||||||
u := url.URL{
|
u := url.URL{
|
||||||
Scheme: "anytls",
|
Scheme: "anytls",
|
||||||
User: url.User(uuid),
|
User: url.User(uuid),
|
||||||
Host: net.JoinHostPort(data.Server, strconv.Itoa(anytls.Port)),
|
Host: net.JoinHostPort(data.Server, strconv.Itoa(data.Port)), // 修复:使用最终端口 data.Port
|
||||||
RawQuery: query.Encode(),
|
RawQuery: query.Encode(),
|
||||||
Fragment: data.Name,
|
Fragment: data.Name,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,157 @@ func adapterProxies(servers []*server.Server) ([]proxy.Proxy, []string, map[stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxies = append(proxies, *n)
|
proxies = append(proxies, *n)
|
||||||
|
case server.RelayModeMixed:
|
||||||
|
// 生成直连节点
|
||||||
|
n := addNode(node, node.ServerAddr, 0)
|
||||||
|
if n != nil {
|
||||||
|
directName := "直连-" + node.Name
|
||||||
|
n.Name = directName
|
||||||
|
if node.Tags != "" {
|
||||||
|
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||||
|
for _, tag := range t {
|
||||||
|
if tag != "" {
|
||||||
|
if _, ok := tags[tag]; !ok {
|
||||||
|
tags[tag] = []string{}
|
||||||
|
}
|
||||||
|
tags[tag] = append(tags[tag], n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies = append(proxies, *n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成一个随机中继节点(类似random模式,但和直连共存)
|
||||||
|
var relays []server.NodeRelay
|
||||||
|
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
|
||||||
|
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(relays) > 0 {
|
||||||
|
randNum := random.RandomInRange(0, len(relays)-1)
|
||||||
|
relay := relays[randNum]
|
||||||
|
n := addNode(node, relay.Host, relay.Port)
|
||||||
|
if n == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
relayName := "中继-" + node.Name
|
||||||
|
if relay.Prefix != "" {
|
||||||
|
relayName = relay.Prefix + node.Name
|
||||||
|
}
|
||||||
|
n.Name = relayName
|
||||||
|
if node.Tags != "" {
|
||||||
|
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||||
|
for _, tag := range t {
|
||||||
|
if tag != "" {
|
||||||
|
if _, ok := tags[tag]; !ok {
|
||||||
|
tags[tag] = []string{}
|
||||||
|
}
|
||||||
|
tags[tag] = append(tags[tag], n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies = append(proxies, *n)
|
||||||
|
}
|
||||||
|
case server.RelayModeAllDirect:
|
||||||
|
// 生成直连节点
|
||||||
|
n := addNode(node, node.ServerAddr, 0)
|
||||||
|
if n != nil {
|
||||||
|
directName := "直连-" + node.Name
|
||||||
|
n.Name = directName
|
||||||
|
if node.Tags != "" {
|
||||||
|
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||||
|
for _, tag := range t {
|
||||||
|
if tag != "" {
|
||||||
|
if _, ok := tags[tag]; !ok {
|
||||||
|
tags[tag] = []string{}
|
||||||
|
}
|
||||||
|
tags[tag] = append(tags[tag], n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies = append(proxies, *n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成所有中继节点
|
||||||
|
var relays []server.NodeRelay
|
||||||
|
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
|
||||||
|
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, relay := range relays {
|
||||||
|
n := addNode(node, relay.Host, relay.Port)
|
||||||
|
if n == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
relayName := "中继-" + node.Name
|
||||||
|
if relay.Prefix != "" {
|
||||||
|
relayName = relay.Prefix + node.Name
|
||||||
|
}
|
||||||
|
n.Name = relayName
|
||||||
|
if node.Tags != "" {
|
||||||
|
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||||
|
for _, tag := range t {
|
||||||
|
if tag != "" {
|
||||||
|
if _, ok := tags[tag]; !ok {
|
||||||
|
tags[tag] = []string{}
|
||||||
|
}
|
||||||
|
tags[tag] = append(tags[tag], n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies = append(proxies, *n)
|
||||||
|
}
|
||||||
|
case server.RelayModeRandomDirect:
|
||||||
|
// 生成直连节点
|
||||||
|
n := addNode(node, node.ServerAddr, 0)
|
||||||
|
if n != nil {
|
||||||
|
directName := "直连-" + node.Name
|
||||||
|
n.Name = directName
|
||||||
|
if node.Tags != "" {
|
||||||
|
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||||
|
for _, tag := range t {
|
||||||
|
if tag != "" {
|
||||||
|
if _, ok := tags[tag]; !ok {
|
||||||
|
tags[tag] = []string{}
|
||||||
|
}
|
||||||
|
tags[tag] = append(tags[tag], n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies = append(proxies, *n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 随机选择一个中继节点
|
||||||
|
var relays []server.NodeRelay
|
||||||
|
if err := json.Unmarshal([]byte(node.RelayNode), &relays); err != nil {
|
||||||
|
logger.Errorw("Unmarshal RelayNode", logger.Field("error", err.Error()), logger.Field("node", node.Name), logger.Field("relayNode", node.RelayNode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(relays) > 0 {
|
||||||
|
randNum := random.RandomInRange(0, len(relays)-1)
|
||||||
|
relay := relays[randNum]
|
||||||
|
n := addNode(node, relay.Host, relay.Port)
|
||||||
|
if n == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
relayName := "中继-" + node.Name
|
||||||
|
if relay.Prefix != "" {
|
||||||
|
relayName = relay.Prefix + node.Name
|
||||||
|
}
|
||||||
|
n.Name = relayName
|
||||||
|
if node.Tags != "" {
|
||||||
|
t := tool.RemoveDuplicateElements(strings.Split(node.Tags, ",")...)
|
||||||
|
for _, tag := range t {
|
||||||
|
if tag != "" {
|
||||||
|
if _, ok := tags[tag]; !ok {
|
||||||
|
tags[tag] = []string{}
|
||||||
|
}
|
||||||
|
tags[tag] = append(tags[tag], n.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxies = append(proxies, *n)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
|
logger.Info("Not Relay Mode", logger.Field("node", node.Name), logger.Field("relayMode", node.RelayMode))
|
||||||
n := addNode(node, node.ServerAddr, 0)
|
n := addNode(node, node.ServerAddr, 0)
|
||||||
|
|||||||
@@ -188,8 +188,10 @@ func (l *CheckSubscriptionLogic) sendTrafficNotify(ctx context.Context, subs []i
|
|||||||
}
|
}
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
err = tpl.Execute(&result, map[string]interface{}{
|
err = tpl.Execute(&result, map[string]interface{}{
|
||||||
"SiteLogo": l.svc.Config.Site.SiteLogo,
|
"SiteLogo": l.svc.Config.Site.SiteLogo,
|
||||||
"SiteName": l.svc.Config.Site.SiteName,
|
"SiteName": l.svc.Config.Site.SiteName,
|
||||||
|
"UsedTraffic": sub.Upload + sub.Download,
|
||||||
|
"MaxTraffic": sub.Traffic,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error()))
|
logger.Errorw("[CheckSubscription] Execute template failed", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 简洁的代码对比脚本
|
||||||
|
echo "🔍 代码对比报告"
|
||||||
|
echo "==========================================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 检查old远程仓库是否存在
|
||||||
|
if ! git remote | grep -q "old"; then
|
||||||
|
echo "❌ 请先添加old远程仓库: git remote add old https://github.com/perfect-panel/server.git && git fetch old"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 文件变更对比:"
|
||||||
|
echo "-------------------------------------------"
|
||||||
|
echo "我们修改的文件:"
|
||||||
|
git diff --name-status old/master main | grep -E '^[AM]' | sed 's/^A/🆕 新增: /' | sed 's/^M/✏️ 修改: /'
|
||||||
|
echo ""
|
||||||
|
echo "他们修改的文件:"
|
||||||
|
git diff --name-status main old/master | grep -E '^[AM]' | sed 's/^A/🆕 新增: /' | sed 's/^M/✏️ 修改: /'
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "📊 变更统计:"
|
||||||
|
echo "-------------------------------------------"
|
||||||
|
echo "我们的变更:"
|
||||||
|
git diff --stat --color=always old/master main | tail -1
|
||||||
|
echo "他们的变更:"
|
||||||
|
git diff --stat --color=always main old/master | tail -1
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "✅ 对比完成!"
|
||||||
Reference in New Issue
Block a user