Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cfca8ef6b | |||
| f9fa4756e9 | |||
| 27f1203282 | |||
| f7f890c990 | |||
| 8b4e7561f4 |
+128
-30
@@ -21,8 +21,8 @@ env:
|
|||||||
# SSH私钥(Gitea Secret 名称:AWS)
|
# SSH私钥(Gitea Secret 名称:AWS)
|
||||||
SSH_KEY: ${{ secrets.AWS }}
|
SSH_KEY: ${{ secrets.AWS }}
|
||||||
# TG通知
|
# TG通知
|
||||||
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }}
|
||||||
TG_CHAT_ID: "-4940243803"
|
TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }}
|
||||||
# Go构建变量
|
# Go构建变量
|
||||||
SERVICE: vpn
|
SERVICE: vpn
|
||||||
SERVICE_STYLE: vpn
|
SERVICE_STYLE: vpn
|
||||||
@@ -51,16 +51,19 @@ jobs:
|
|||||||
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
|
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
|
||||||
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
|
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
|
||||||
echo "DEPLOY_PATH=/opt/ppanel" >> $GITHUB_ENV
|
echo "DEPLOY_PATH=/opt/ppanel" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_ENV_LABEL=🚀 服务已成功部署到生产环境" >> $GITHUB_ENV
|
||||||
echo "为 main 分支设置生产环境变量"
|
echo "为 main 分支设置生产环境变量"
|
||||||
elif [ "${{ github.ref_name }}" = "internal" ]; then
|
elif [ "${{ github.ref_name }}" = "internal" ]; then
|
||||||
echo "DOCKER_TAG_SUFFIX=internal" >> $GITHUB_ENV
|
echo "DOCKER_TAG_SUFFIX=internal" >> $GITHUB_ENV
|
||||||
echo "CONTAINER_NAME=ppanel-server-internal" >> $GITHUB_ENV
|
echo "CONTAINER_NAME=ppanel-server-internal" >> $GITHUB_ENV
|
||||||
echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
|
echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_ENV_LABEL=🧪 服务已成功部署到测试环境" >> $GITHUB_ENV
|
||||||
echo "为 internal 分支设置开发环境变量"
|
echo "为 internal 分支设置开发环境变量"
|
||||||
else
|
else
|
||||||
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
|
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
|
||||||
echo "CONTAINER_NAME=ppanel-server-${{ 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 "DEPLOY_PATH=/root/vpn_server_other" >> $GITHUB_ENV
|
||||||
|
echo "DEPLOY_ENV_LABEL=🔧 服务已成功部署到其他环境" >> $GITHUB_ENV
|
||||||
echo "为其他分支 (${{ github.ref_name }}) 设置环境变量"
|
echo "为其他分支 (${{ github.ref_name }}) 设置环境变量"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -112,24 +115,37 @@ jobs:
|
|||||||
docker version || true
|
docker version || true
|
||||||
echo "客户端 API 版本:" $(docker version --format '{{.Client.APIVersion}}')
|
echo "客户端 API 版本:" $(docker version --format '{{.Client.APIVersion}}')
|
||||||
|
|
||||||
# 步骤4: 构建并发布到镜像仓库
|
# 步骤4: 构建镜像
|
||||||
- name: 📤 构建并发布到镜像仓库
|
- name: 🏗️ 构建镜像
|
||||||
run: |
|
run: |
|
||||||
echo "开始构建并推送镜像..."
|
echo "开始构建镜像..."
|
||||||
echo "仓库: ${{ env.REPO }}"
|
echo "仓库: ${{ env.REPO }}"
|
||||||
echo "版本标签: ${{ env.VERSION }}"
|
echo "版本标签: ${{ env.VERSION }}"
|
||||||
echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}"
|
echo "分支标签: ${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
|
|
||||||
# 构建镜像,同时打上版本和分支两个标签
|
BUILD_TAG_ARGS="-t ${{ env.REPO }}:${{ env.VERSION }}"
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
BUILD_TAG_ARGS="$BUILD_TAG_ARGS -t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
|
else
|
||||||
|
echo "PR事件仅构建版本标签,不推送镜像、不部署"
|
||||||
|
fi
|
||||||
|
|
||||||
docker build -f Dockerfile \
|
docker build -f Dockerfile \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
--build-arg TARGETARCH=amd64 \
|
--build-arg TARGETARCH=amd64 \
|
||||||
--build-arg VERSION=${{ env.VERSION }} \
|
--build-arg VERSION=${{ env.VERSION }} \
|
||||||
--build-arg BUILDTIME=${{ env.BUILDTIME }} \
|
--build-arg BUILDTIME=${{ env.BUILDTIME }} \
|
||||||
-t ${{ env.REPO }}:${{ env.VERSION }} \
|
$BUILD_TAG_ARGS \
|
||||||
-t ${{ env.REPO }}:${{ env.DOCKER_TAG_SUFFIX }} \
|
|
||||||
.
|
.
|
||||||
|
|
||||||
|
echo "镜像构建完成"
|
||||||
|
|
||||||
|
# 步骤5: 发布到镜像仓库
|
||||||
|
- name: 📤 发布到镜像仓库
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
run: |
|
||||||
|
echo "开始推送镜像..."
|
||||||
|
|
||||||
echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}"
|
echo "推送版本标签镜像: ${{ env.REPO }}:${{ env.VERSION }}"
|
||||||
docker push ${{ env.REPO }}:${{ env.VERSION }}
|
docker push ${{ env.REPO }}:${{ env.VERSION }}
|
||||||
|
|
||||||
@@ -138,8 +154,9 @@ jobs:
|
|||||||
|
|
||||||
echo "镜像推送完成"
|
echo "镜像推送完成"
|
||||||
|
|
||||||
# 调试: 打印部署目标(不输出敏感信息)
|
# 步骤6: 调试 - 打印部署目标(不输出敏感信息)
|
||||||
- name: 🔍 调试 - 打印部署目标
|
- name: 🔍 调试 - 打印部署目标
|
||||||
|
if: github.event_name == 'push'
|
||||||
run: |
|
run: |
|
||||||
echo "========== 部署目标调试 =========="
|
echo "========== 部署目标调试 =========="
|
||||||
echo "当前分支: ${{ github.ref_name }}"
|
echo "当前分支: ${{ github.ref_name }}"
|
||||||
@@ -150,8 +167,9 @@ jobs:
|
|||||||
echo "DEPLOY_PATH: ${{ env.DEPLOY_PATH }}"
|
echo "DEPLOY_PATH: ${{ env.DEPLOY_PATH }}"
|
||||||
echo "====================================="
|
echo "====================================="
|
||||||
|
|
||||||
# 步骤5: 传输配置文件
|
# 步骤7: 传输配置文件
|
||||||
- name: 📂 传输配置文件
|
- name: 📂 传输配置文件
|
||||||
|
if: github.event_name == 'push'
|
||||||
uses: appleboy/scp-action@v0.1.7
|
uses: appleboy/scp-action@v0.1.7
|
||||||
with:
|
with:
|
||||||
host: ${{ env.SSH_HOST }}
|
host: ${{ env.SSH_HOST }}
|
||||||
@@ -161,8 +179,9 @@ jobs:
|
|||||||
source: "docker-compose.cloud.yml"
|
source: "docker-compose.cloud.yml"
|
||||||
target: "/tmp/ppanel-deploy/"
|
target: "/tmp/ppanel-deploy/"
|
||||||
|
|
||||||
# 步骤6: 连接服务器更新并启动
|
# 步骤8: 连接服务器更新、健康检查并按需回滚
|
||||||
- name: 🚀 连接服务器更新并启动
|
- name: 🚀 连接服务器更新并启动
|
||||||
|
if: github.event_name == 'push'
|
||||||
uses: appleboy/ssh-action@v1.0.3
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
with:
|
with:
|
||||||
host: ${{ env.SSH_HOST }}
|
host: ${{ env.SSH_HOST }}
|
||||||
@@ -172,36 +191,114 @@ jobs:
|
|||||||
timeout: 300s
|
timeout: 300s
|
||||||
command_timeout: 600s
|
command_timeout: 600s
|
||||||
script: |
|
script: |
|
||||||
|
set -e
|
||||||
|
|
||||||
echo "连接服务器成功,开始部署..."
|
echo "连接服务器成功,开始部署..."
|
||||||
echo "部署目录: ${{ env.DEPLOY_PATH }}"
|
echo "部署目录: ${{ env.DEPLOY_PATH }}"
|
||||||
echo "部署标签: ${{ env.DOCKER_TAG_SUFFIX }}"
|
echo "部署标签: ${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
echo "登录用户: ${{ env.SSH_USER }}"
|
echo "登录用户: ${{ env.SSH_USER }}"
|
||||||
|
|
||||||
|
HEALTHCHECK_URL="http://127.0.0.1:8080/v1/common/heartbeat"
|
||||||
|
NEW_TAG="${{ env.DOCKER_TAG_SUFFIX }}"
|
||||||
|
ROLLBACK_TAG="rollback-${{ env.VERSION }}"
|
||||||
|
SUDO=""
|
||||||
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker_cmd() {
|
||||||
|
if [ -n "$SUDO" ]; then
|
||||||
|
sudo docker "$@"
|
||||||
|
else
|
||||||
|
docker "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
compose_with_tag() {
|
||||||
|
tag="$1"
|
||||||
|
shift
|
||||||
|
if [ -n "$SUDO" ]; then
|
||||||
|
sudo env PPANEL_SERVER_TAG="$tag" docker-compose -f docker-compose.cloud.yml "$@"
|
||||||
|
else
|
||||||
|
PPANEL_SERVER_TAG="$tag" docker-compose -f docker-compose.cloud.yml "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_previous_tag() {
|
||||||
|
if [ -n "$SUDO" ]; then
|
||||||
|
printf '%s\n' "${PREVIOUS_IMAGE_TAG:-}" | sudo tee .previous-ppanel-image-tag >/dev/null
|
||||||
|
else
|
||||||
|
printf '%s\n' "${PREVIOUS_IMAGE_TAG:-}" > .previous-ppanel-image-tag
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
health_check() {
|
||||||
|
attempt=1
|
||||||
|
while [ "$attempt" -le 3 ]; do
|
||||||
|
if curl -sf "$HEALTHCHECK_URL"; then
|
||||||
|
echo
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "健康检查第 ${attempt}/3 次失败,10s 后重试..."
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
if [ "${{ github.ref_name }}" = "main" ]; then
|
if [ "${{ github.ref_name }}" = "main" ]; then
|
||||||
sudo mkdir -p ${{ env.DEPLOY_PATH }}
|
sudo mkdir -p ${{ env.DEPLOY_PATH }}
|
||||||
sudo cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml
|
sudo cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml
|
||||||
cd ${{ env.DEPLOY_PATH }}
|
|
||||||
echo "📥 拉取镜像..."
|
|
||||||
sudo docker-compose -f docker-compose.cloud.yml pull ppanel-server
|
|
||||||
echo "🚀 启动服务..."
|
|
||||||
sudo docker-compose -f docker-compose.cloud.yml up -d ppanel-server
|
|
||||||
sudo docker image prune -f || true
|
|
||||||
else
|
else
|
||||||
mkdir -p ${{ env.DEPLOY_PATH }}
|
mkdir -p ${{ env.DEPLOY_PATH }}
|
||||||
cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml
|
cp /tmp/ppanel-deploy/docker-compose.cloud.yml ${{ env.DEPLOY_PATH }}/docker-compose.cloud.yml
|
||||||
cd ${{ env.DEPLOY_PATH }}
|
|
||||||
echo "📥 拉取镜像..."
|
|
||||||
docker-compose -f docker-compose.cloud.yml pull ppanel-server
|
|
||||||
echo "🚀 启动服务..."
|
|
||||||
docker-compose -f docker-compose.cloud.yml up -d ppanel-server
|
|
||||||
docker image prune -f || true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ 部署命令执行完成"
|
cd ${{ env.DEPLOY_PATH }}
|
||||||
|
|
||||||
# 步骤6: TG通知 (成功)
|
PREVIOUS_IMAGE_TAG="$(docker_cmd inspect --format '{{.Config.Image}}' ppanel-server 2>/dev/null || true)"
|
||||||
|
PREVIOUS_IMAGE_ID="$(docker_cmd inspect --format '{{.Image}}' ppanel-server 2>/dev/null || true)"
|
||||||
|
echo "上一版本镜像tag: ${PREVIOUS_IMAGE_TAG:-未发现}"
|
||||||
|
echo "上一版本镜像ID: ${PREVIOUS_IMAGE_ID:-未发现}"
|
||||||
|
write_previous_tag
|
||||||
|
|
||||||
|
echo "📥 拉取镜像: ${{ env.REPO }}:${NEW_TAG}"
|
||||||
|
compose_with_tag "$NEW_TAG" pull ppanel-server
|
||||||
|
echo "🚀 启动服务..."
|
||||||
|
compose_with_tag "$NEW_TAG" up -d ppanel-server
|
||||||
|
|
||||||
|
echo "🩺 部署后健康检查: ${HEALTHCHECK_URL}"
|
||||||
|
if health_check; then
|
||||||
|
docker_cmd image prune -f || true
|
||||||
|
echo "✅ 部署后健康检查通过"
|
||||||
|
echo "✅ 部署命令执行完成"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "❌ 部署后健康检查连续 3 次失败,开始回滚..."
|
||||||
|
if [ -n "$PREVIOUS_IMAGE_ID" ]; then
|
||||||
|
docker_cmd tag "$PREVIOUS_IMAGE_ID" "${{ env.REPO }}:${ROLLBACK_TAG}"
|
||||||
|
echo "回滚镜像tag: ${{ env.REPO }}:${ROLLBACK_TAG}"
|
||||||
|
compose_with_tag "$ROLLBACK_TAG" up -d ppanel-server
|
||||||
|
|
||||||
|
echo "🩺 回滚后健康检查: ${HEALTHCHECK_URL}"
|
||||||
|
if health_check; then
|
||||||
|
echo "✅ 回滚后健康检查通过"
|
||||||
|
else
|
||||||
|
echo "❌ 回滚后健康检查仍失败"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "未找到上一版本镜像ID,无法自动回滚"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker_cmd image prune -f || true
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# 步骤9: TG通知 (成功)
|
||||||
- name: 📱 发送成功通知到Telegram
|
- name: 📱 发送成功通知到Telegram
|
||||||
if: success()
|
if: success() && github.event_name == 'push'
|
||||||
uses: appleboy/telegram-action@master
|
uses: appleboy/telegram-action@master
|
||||||
with:
|
with:
|
||||||
token: ${{ env.TG_BOT_TOKEN }}
|
token: ${{ env.TG_BOT_TOKEN }}
|
||||||
@@ -215,12 +312,13 @@ jobs:
|
|||||||
👤 提交者: ${{ github.actor }}
|
👤 提交者: ${{ github.actor }}
|
||||||
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
🚀 服务已成功部署到生产环境
|
${{ env.DEPLOY_ENV_LABEL }}
|
||||||
|
🩺 健康检查: 通过 (http://127.0.0.1:8080/v1/common/heartbeat)
|
||||||
parse_mode: Markdown
|
parse_mode: Markdown
|
||||||
|
|
||||||
# 步骤5: TG通知 (失败)
|
# 步骤10: TG通知 (失败)
|
||||||
- name: 📱 发送失败通知到Telegram
|
- name: 📱 发送失败通知到Telegram
|
||||||
if: failure()
|
if: failure() && github.event_name == 'push'
|
||||||
uses: appleboy/telegram-action@master
|
uses: appleboy/telegram-action@master
|
||||||
with:
|
with:
|
||||||
token: ${{ env.TG_BOT_TOKEN }}
|
token: ${{ env.TG_BOT_TOKEN }}
|
||||||
@@ -234,6 +332,6 @@ jobs:
|
|||||||
👤 提交者: ${{ github.actor }}
|
👤 提交者: ${{ github.actor }}
|
||||||
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
🕐 时间: ${{ github.event.head_commit.timestamp }}
|
||||||
|
|
||||||
|
🩺 健康检查: 失败或未完成;若新版本健康检查连续 3 次失败,已自动尝试回滚并重新检查
|
||||||
⚠️ 请检查构建日志获取详细信息
|
⚠️ 请检查构建日志获取详细信息
|
||||||
parse_mode: Markdown
|
parse_mode: Markdown
|
||||||
|
|
||||||
|
|||||||
+6
-5
@@ -41,17 +41,17 @@ type (
|
|||||||
UserId int64 `json:"user_id" validate:"required"`
|
UserId int64 `json:"user_id" validate:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Balance int64 `json:"balance"`
|
Balance *int64 `json:"balance"`
|
||||||
Commission int64 `json:"commission"`
|
Commission *int64 `json:"commission"`
|
||||||
ReferralPercentage uint8 `json:"referral_percentage"`
|
ReferralPercentage uint8 `json:"referral_percentage"`
|
||||||
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
||||||
GiftAmount int64 `json:"gift_amount"`
|
GiftAmount *int64 `json:"gift_amount"`
|
||||||
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"`
|
||||||
Enable *bool `json:"enable"`
|
Enable *bool `json:"enable"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Remark string `json:"remark"`
|
Remark *string `json:"remark"`
|
||||||
}
|
}
|
||||||
UpdateUserNotifySettingRequest {
|
UpdateUserNotifySettingRequest {
|
||||||
UserId int64 `json:"user_id" validate:"required"`
|
UserId int64 `json:"user_id" validate:"required"`
|
||||||
@@ -235,6 +235,7 @@ type (
|
|||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
UserId *int64 `form:"user_id,omitempty"`
|
UserId *int64 `form:"user_id,omitempty"`
|
||||||
Status *uint8 `form:"status,omitempty"`
|
Status *uint8 `form:"status,omitempty"`
|
||||||
|
Method *uint8 `form:"method,omitempty"`
|
||||||
}
|
}
|
||||||
GetWithdrawalListResponse {
|
GetWithdrawalListResponse {
|
||||||
List []WithdrawalLog `json:"list"`
|
List []WithdrawalLog `json:"list"`
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ type (
|
|||||||
CommissionWithdrawRequest {
|
CommissionWithdrawRequest {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Method uint8 `json:"method" validate:"oneof=0 1 2 3"`
|
||||||
|
Account string `json:"account,omitempty"`
|
||||||
|
QrCodeUrl string `json:"qr_code_url,omitempty"`
|
||||||
}
|
}
|
||||||
WithdrawalLog {
|
WithdrawalLog {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
@@ -119,9 +122,15 @@ type (
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Method uint8 `json:"method"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
QrCodeUrl string `json:"qr_code_url"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
CancelWithdrawalRequest {
|
||||||
|
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
QueryWithdrawalLogListRequest {
|
QueryWithdrawalLogListRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@@ -352,6 +361,10 @@ service ppanel {
|
|||||||
@handler CommissionWithdraw
|
@handler CommissionWithdraw
|
||||||
post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog)
|
post /commission_withdraw (CommissionWithdrawRequest) returns (WithdrawalLog)
|
||||||
|
|
||||||
|
@doc "Cancel pending withdrawal"
|
||||||
|
@handler CancelWithdrawal
|
||||||
|
post /withdrawal_cancel (CancelWithdrawalRequest) returns (WithdrawalLog)
|
||||||
|
|
||||||
@doc "Query Withdrawal Log"
|
@doc "Query Withdrawal Log"
|
||||||
@handler QueryWithdrawalLog
|
@handler QueryWithdrawalLog
|
||||||
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
get /withdrawal_log (QueryWithdrawalLogListRequest) returns (QueryWithdrawalLogListResponse)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE `withdrawals`
|
||||||
|
DROP COLUMN `qr_code_url`,
|
||||||
|
DROP COLUMN `account`,
|
||||||
|
DROP COLUMN `method`;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE `withdrawals`
|
||||||
|
ADD COLUMN `method` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '收款方式 0:其他 1:支付宝 2:微信 3:银行卡' AFTER `content`,
|
||||||
|
ADD COLUMN `account` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '收款账号' AFTER `method`,
|
||||||
|
ADD COLUMN `qr_code_url` VARCHAR(500) NOT NULL DEFAULT '' COMMENT '收款码图片URL' AFTER `account`;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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/internal/types"
|
||||||
|
"github.com/perfect-panel/server/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cancel Withdrawal
|
||||||
|
func CancelWithdrawalHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
var req types.CancelWithdrawalRequest
|
||||||
|
_ = c.ShouldBind(&req)
|
||||||
|
validateErr := svcCtx.Validate(&req)
|
||||||
|
if validateErr != nil {
|
||||||
|
result.ParamErrorResult(c, validateErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l := user.NewCancelWithdrawalLogic(c.Request.Context(), svcCtx)
|
||||||
|
resp, err := l.CancelWithdrawal(&req)
|
||||||
|
result.HttpResult(c, resp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1059,6 +1059,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
|||||||
// Commission Withdraw
|
// Commission Withdraw
|
||||||
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
publicUserGroupRouter.POST("/commission_withdraw", publicUser.CommissionWithdrawHandler(serverCtx))
|
||||||
|
|
||||||
|
// Cancel Withdrawal
|
||||||
|
publicUserGroupRouter.POST("/withdrawal_cancel", publicUser.CancelWithdrawalHandler(serverCtx))
|
||||||
|
|
||||||
// Delete Current User Account
|
// Delete Current User Account
|
||||||
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
publicUserGroupRouter.DELETE("/current_user_account", publicUser.DeleteCurrentUserAccountHandler(serverCtx))
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
|
|||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
query = query.Where("status = ?", *req.Status)
|
query = query.Where("status = ?", *req.Status)
|
||||||
}
|
}
|
||||||
|
if req.Method != nil {
|
||||||
|
query = query.Where("method = ?", *req.Method)
|
||||||
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := query.Count(&total).Error; err != nil {
|
if err := query.Count(&total).Error; err != nil {
|
||||||
@@ -62,6 +65,9 @@ func (l *GetWithdrawalListLogic) GetWithdrawalList(req *types.GetWithdrawalListR
|
|||||||
Content: row.Content,
|
Content: row.Content,
|
||||||
Status: row.Status,
|
Status: row.Status,
|
||||||
Reason: row.Reason,
|
Reason: row.Reason,
|
||||||
|
Method: row.Method,
|
||||||
|
Account: row.Account,
|
||||||
|
QrCodeUrl: row.QrCodeUrl,
|
||||||
CreatedAt: row.CreatedAt.UnixMilli(),
|
CreatedAt: row.CreatedAt.UnixMilli(),
|
||||||
UpdatedAt: row.UpdatedAt.UnixMilli(),
|
UpdatedAt: row.UpdatedAt.UnixMilli(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||||
if userInfo.Balance != req.Balance {
|
if req.Balance != nil && userInfo.Balance != *req.Balance {
|
||||||
change := req.Balance - userInfo.Balance
|
change := *req.Balance - userInfo.Balance
|
||||||
balanceLog := log.Balance{
|
balanceLog := log.Balance{
|
||||||
Type: log.BalanceTypeAdjust,
|
Type: log.BalanceTypeAdjust,
|
||||||
Amount: change,
|
Amount: change,
|
||||||
OrderNo: "",
|
OrderNo: "",
|
||||||
Balance: req.Balance,
|
Balance: *req.Balance,
|
||||||
Timestamp: time.Now().UnixMilli(),
|
Timestamp: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
content, _ := balanceLog.Marshal()
|
content, _ := balanceLog.Marshal()
|
||||||
@@ -66,14 +66,14 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfo.Balance = req.Balance
|
userInfo.Balance = *req.Balance
|
||||||
}
|
}
|
||||||
|
|
||||||
if userInfo.GiftAmount != req.GiftAmount {
|
if req.GiftAmount != nil && userInfo.GiftAmount != *req.GiftAmount {
|
||||||
change := req.GiftAmount - userInfo.GiftAmount
|
change := *req.GiftAmount - userInfo.GiftAmount
|
||||||
if change != 0 {
|
if change != 0 {
|
||||||
var changeType uint16
|
var changeType uint16
|
||||||
if userInfo.GiftAmount < req.GiftAmount {
|
if userInfo.GiftAmount < *req.GiftAmount {
|
||||||
changeType = log.GiftTypeIncrease
|
changeType = log.GiftTypeIncrease
|
||||||
} else {
|
} else {
|
||||||
changeType = log.GiftTypeReduce
|
changeType = log.GiftTypeReduce
|
||||||
@@ -81,7 +81,7 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
giftLog := log.Gift{
|
giftLog := log.Gift{
|
||||||
Type: changeType,
|
Type: changeType,
|
||||||
Amount: change,
|
Amount: change,
|
||||||
Balance: req.GiftAmount,
|
Balance: *req.GiftAmount,
|
||||||
Remark: "Admin adjustment",
|
Remark: "Admin adjustment",
|
||||||
Timestamp: time.Now().UnixMilli(),
|
Timestamp: time.Now().UnixMilli(),
|
||||||
}
|
}
|
||||||
@@ -96,23 +96,27 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfo.GiftAmount = req.GiftAmount
|
userInfo.GiftAmount = *req.GiftAmount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Commission != userInfo.Commission {
|
if req.Commission != nil && *req.Commission != userInfo.Commission {
|
||||||
if isWithdrawalScene(req.Remark) {
|
remark := ""
|
||||||
|
if req.Remark != nil {
|
||||||
|
remark = *req.Remark
|
||||||
|
}
|
||||||
|
if isWithdrawalScene(remark) {
|
||||||
logWithdrawalGuard(l.Logger, userInfo.Id)
|
logWithdrawalGuard(l.Logger, userInfo.Id)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "commission overwrite is blocked in withdrawal scene")
|
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "commission overwrite is blocked in withdrawal scene")
|
||||||
}
|
}
|
||||||
change := req.Commission - userInfo.Commission
|
change := *req.Commission - userInfo.Commission
|
||||||
if err = l.svcCtx.UserModel.UpdateCommission(l.ctx, userInfo.Id, change, tx); err != nil {
|
if err = l.svcCtx.UserModel.UpdateCommission(l.ctx, userInfo.Id, change, tx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = logicCommon.WriteCommissionLog(tx, userInfo.Id, log.CommissionTypeAdjust, change, ""); err != nil {
|
if err = logicCommon.WriteCommissionLog(tx, userInfo.Id, log.CommissionTypeAdjust, change, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userInfo.Commission = req.Commission
|
userInfo.Commission = *req.Commission
|
||||||
}
|
}
|
||||||
if req.Avatar != "" {
|
if req.Avatar != "" {
|
||||||
userInfo.Avatar = req.Avatar
|
userInfo.Avatar = req.Avatar
|
||||||
@@ -120,8 +124,8 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if req.ReferCode != "" {
|
if req.ReferCode != "" {
|
||||||
userInfo.ReferCode = req.ReferCode
|
userInfo.ReferCode = req.ReferCode
|
||||||
}
|
}
|
||||||
if req.RefererId != 0 {
|
if req.RefererId != nil {
|
||||||
userInfo.RefererId = req.RefererId
|
userInfo.RefererId = *req.RefererId
|
||||||
}
|
}
|
||||||
if req.Enable != nil {
|
if req.Enable != nil {
|
||||||
userInfo.Enable = req.Enable
|
userInfo.Enable = req.Enable
|
||||||
@@ -129,8 +133,8 @@ func (l *UpdateUserBasicInfoLogic) UpdateUserBasicInfo(req *types.UpdateUserBasi
|
|||||||
if req.IsAdmin != nil {
|
if req.IsAdmin != nil {
|
||||||
userInfo.IsAdmin = req.IsAdmin
|
userInfo.IsAdmin = req.IsAdmin
|
||||||
}
|
}
|
||||||
if req.Remark != "" {
|
if req.Remark != nil {
|
||||||
userInfo.Remark = req.Remark
|
userInfo.Remark = *req.Remark
|
||||||
}
|
}
|
||||||
if req.OnlyFirstPurchase != nil {
|
if req.OnlyFirstPurchase != nil {
|
||||||
userInfo.OnlyFirstPurchase = req.OnlyFirstPurchase
|
userInfo.OnlyFirstPurchase = req.OnlyFirstPurchase
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ func approveWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdraw
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Model(&usermodel.Withdrawal{}).
|
if err := tx.Model(&usermodel.Withdrawal{}).
|
||||||
Where("id = ? AND status = 0", withdrawalID).
|
Where("id = ? AND status = ?", withdrawalID, usermodel.WithdrawalStatusPending).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"status": 1,
|
"status": usermodel.WithdrawalStatusApproved,
|
||||||
"reason": "",
|
"reason": "",
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "approve withdrawal failed: %v", err)
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "approve withdrawal failed: %v", err)
|
||||||
@@ -80,9 +80,9 @@ func rejectWithdrawal(ctx context.Context, svcCtx *svc.ServiceContext, withdrawa
|
|||||||
// Commission was NOT deducted at application time under the new logic,
|
// Commission was NOT deducted at application time under the new logic,
|
||||||
// so rejection requires no refund — only a status update.
|
// so rejection requires no refund — only a status update.
|
||||||
return tx.Model(&usermodel.Withdrawal{}).
|
return tx.Model(&usermodel.Withdrawal{}).
|
||||||
Where("id = ? AND status = 0", withdrawalID).
|
Where("id = ? AND status = ?", withdrawalID, usermodel.WithdrawalStatusPending).
|
||||||
Updates(map[string]interface{}{
|
Updates(map[string]interface{}{
|
||||||
"status": 2,
|
"status": usermodel.WithdrawalStatusRejected,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
}).Error
|
}).Error
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func LoadPendingWithdrawalForUpdate(ctx context.Context, tx *gorm.DB, withdrawal
|
|||||||
First(&withdrawal).Error; err != nil {
|
First(&withdrawal).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if withdrawal.Status != 0 {
|
if withdrawal.Status != usermodel.WithdrawalStatusPending {
|
||||||
return nil, errors.New("withdrawal status invalid")
|
return nil, errors.New("withdrawal status invalid")
|
||||||
}
|
}
|
||||||
return &withdrawal, nil
|
return &withdrawal, nil
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
logicCommon "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"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CancelWithdrawalLogic struct {
|
||||||
|
logger.Logger
|
||||||
|
ctx context.Context
|
||||||
|
svcCtx *svc.ServiceContext
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCancelWithdrawalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelWithdrawalLogic {
|
||||||
|
return &CancelWithdrawalLogic{
|
||||||
|
Logger: logger.WithContext(ctx),
|
||||||
|
ctx: ctx,
|
||||||
|
svcCtx: svcCtx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *CancelWithdrawalLogic) CancelWithdrawal(req *types.CancelWithdrawalRequest) (resp *types.WithdrawalLog, err error) {
|
||||||
|
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||||
|
if !ok {
|
||||||
|
l.Error("current user is not found in context")
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
|
}
|
||||||
|
|
||||||
|
var withdrawal *user.Withdrawal
|
||||||
|
err = l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var txErr error
|
||||||
|
withdrawal, txErr = logicCommon.LoadPendingWithdrawalForUpdate(l.ctx, tx, req.WithdrawalId)
|
||||||
|
if txErr != nil {
|
||||||
|
if txErr.Error() == "withdrawal status invalid" {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.WithdrawalStatusInvalid), "withdrawal %d is not in pending state", req.WithdrawalId)
|
||||||
|
}
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "load withdrawal failed: %v", txErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if withdrawal.UserId != u.Id {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.PermissionDenied), "user %d cannot cancel withdrawal belonging to user %d", u.Id, withdrawal.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if txErr = tx.Model(&user.Withdrawal{}).
|
||||||
|
Where("id = ? AND status = ?", req.WithdrawalId, user.WithdrawalStatusPending).
|
||||||
|
Update("status", user.WithdrawalStatusCancelled).Error; txErr != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "cancel withdrawal failed: %v", txErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if txErr = l.svcCtx.UserModel.UpdateCommission(l.ctx, withdrawal.UserId, withdrawal.Amount, tx); txErr != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund commission failed: %v", txErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if txErr = logicCommon.WriteCommissionLog(tx, withdrawal.UserId, log.CommissionTypeWithdrawCancel, withdrawal.Amount, ""); txErr != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "write commission log failed: %v", txErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u)
|
||||||
|
|
||||||
|
return &types.WithdrawalLog{
|
||||||
|
Id: withdrawal.Id,
|
||||||
|
UserId: withdrawal.UserId,
|
||||||
|
Amount: withdrawal.Amount,
|
||||||
|
Content: withdrawal.Content,
|
||||||
|
Status: user.WithdrawalStatusCancelled,
|
||||||
|
Reason: "",
|
||||||
|
Method: withdrawal.Method,
|
||||||
|
Account: withdrawal.Account,
|
||||||
|
QrCodeUrl: withdrawal.QrCodeUrl,
|
||||||
|
CreatedAt: withdrawal.CreatedAt.UnixMilli(),
|
||||||
|
UpdatedAt: withdrawal.UpdatedAt.UnixMilli(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -35,12 +35,28 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate payment method fields
|
||||||
|
switch req.Method {
|
||||||
|
case user.WithdrawalMethodAlipay, user.WithdrawalMethodWechat:
|
||||||
|
if req.QrCodeUrl == "" {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "qr_code_url is required for method %d", req.Method)
|
||||||
|
}
|
||||||
|
case user.WithdrawalMethodBank:
|
||||||
|
if req.Account == "" {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account is required for bank transfer")
|
||||||
|
}
|
||||||
|
default: // WithdrawalMethodOther
|
||||||
|
if req.Account == "" && req.Content == "" {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "account or content is required for other methods")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Sum all pending (status=0) withdrawals to compute available balance.
|
// Sum all pending (status=0) withdrawals to compute available balance.
|
||||||
// Available = commission - pendingTotal; commission is only deducted on approval.
|
// Available = commission - pendingTotal; commission is only deducted on approval.
|
||||||
var pendingTotal int64
|
var pendingTotal int64
|
||||||
if err = l.svcCtx.DB.WithContext(l.ctx).
|
if err = l.svcCtx.DB.WithContext(l.ctx).
|
||||||
Model(&user.Withdrawal{}).
|
Model(&user.Withdrawal{}).
|
||||||
Where("user_id = ? AND status = 0", u.Id).
|
Where("user_id = ? AND status = ?", u.Id, user.WithdrawalStatusPending).
|
||||||
Select("COALESCE(SUM(amount), 0)").
|
Select("COALESCE(SUM(amount), 0)").
|
||||||
Scan(&pendingTotal).Error; err != nil {
|
Scan(&pendingTotal).Error; err != nil {
|
||||||
l.Errorf("Failed to query pending withdrawals for user %d: %v", u.Id, err)
|
l.Errorf("Failed to query pending withdrawals for user %d: %v", u.Id, err)
|
||||||
@@ -59,8 +75,11 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
|
|||||||
UserId: u.Id,
|
UserId: u.Id,
|
||||||
Amount: req.Amount,
|
Amount: req.Amount,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
Status: 0,
|
Status: user.WithdrawalStatusPending,
|
||||||
Reason: "",
|
Reason: "",
|
||||||
|
Method: req.Method,
|
||||||
|
Account: req.Account,
|
||||||
|
QrCodeUrl: req.QrCodeUrl,
|
||||||
}
|
}
|
||||||
return tx.Create(&w).Error
|
return tx.Create(&w).Error
|
||||||
})
|
})
|
||||||
@@ -70,11 +89,15 @@ func (l *CommissionWithdrawLogic) CommissionWithdraw(req *types.CommissionWithdr
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &types.WithdrawalLog{
|
return &types.WithdrawalLog{
|
||||||
|
Id: w.Id,
|
||||||
UserId: u.Id,
|
UserId: u.Id,
|
||||||
Amount: req.Amount,
|
Amount: req.Amount,
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
Status: 0,
|
Status: user.WithdrawalStatusPending,
|
||||||
Reason: "",
|
Reason: "",
|
||||||
|
Method: req.Method,
|
||||||
|
Account: req.Account,
|
||||||
|
QrCodeUrl: req.QrCodeUrl,
|
||||||
CreatedAt: w.CreatedAt.UnixMilli(),
|
CreatedAt: w.CreatedAt.UnixMilli(),
|
||||||
UpdatedAt: w.UpdatedAt.UnixMilli(),
|
UpdatedAt: w.UpdatedAt.UnixMilli(),
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ func (l *QueryWithdrawalLogLogic) QueryWithdrawalLog(req *types.QueryWithdrawalL
|
|||||||
Content: row.Content,
|
Content: row.Content,
|
||||||
Status: row.Status,
|
Status: row.Status,
|
||||||
Reason: row.Reason,
|
Reason: row.Reason,
|
||||||
|
Method: row.Method,
|
||||||
|
Account: row.Account,
|
||||||
|
QrCodeUrl: row.QrCodeUrl,
|
||||||
CreatedAt: row.CreatedAt.UnixMilli(),
|
CreatedAt: row.CreatedAt.UnixMilli(),
|
||||||
UpdatedAt: row.UpdatedAt.UnixMilli(),
|
UpdatedAt: row.UpdatedAt.UnixMilli(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const (
|
|||||||
CommissionTypeAdjust uint16 = 335 // Admin Adjust
|
CommissionTypeAdjust uint16 = 335 // Admin Adjust
|
||||||
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
|
CommissionTypeConvertBalance uint16 = 336 // Convert to Balance
|
||||||
CommissionTypeWithdrawReject uint16 = 337 // Withdraw rejected refund
|
CommissionTypeWithdrawReject uint16 = 337 // Withdraw rejected refund
|
||||||
|
CommissionTypeWithdrawCancel uint16 = 338 // 用户取消提现退佣金
|
||||||
GiftTypeIncrease uint16 = 341 // Increase
|
GiftTypeIncrease uint16 = 341 // Increase
|
||||||
GiftTypeReduce uint16 = 342 // Reduce
|
GiftTypeReduce uint16 = 342 // Reduce
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -160,8 +160,8 @@ func (m *customOrderModel) QueryMonthlyOrders(ctx context.Context, date time.Tim
|
|||||||
Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance").
|
Where("status IN ? AND created_at BETWEEN ? AND ? AND method != ?", []int64{2, 5}, firstDay, lastDay, "balance").
|
||||||
Select(
|
Select(
|
||||||
"SUM(amount) as amount_total, " +
|
"SUM(amount) as amount_total, " +
|
||||||
"SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) as new_order_amount, " +
|
"SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " +
|
||||||
"SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) as renewal_order_amount",
|
"SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount",
|
||||||
).
|
).
|
||||||
Scan(v).Error
|
Scan(v).Error
|
||||||
})
|
})
|
||||||
@@ -177,8 +177,8 @@ func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time)
|
|||||||
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?", []int64{2, 5}, dateStr, "balance").
|
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?", []int64{2, 5}, dateStr, "balance").
|
||||||
Select(
|
Select(
|
||||||
"SUM(amount) as amount_total, " +
|
"SUM(amount) as amount_total, " +
|
||||||
"SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) as new_order_amount, " +
|
"SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) as new_order_amount, " +
|
||||||
"SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) as renewal_order_amount",
|
"SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) as renewal_order_amount",
|
||||||
).
|
).
|
||||||
Scan(v).Error
|
Scan(v).Error
|
||||||
})
|
})
|
||||||
@@ -192,8 +192,8 @@ func (m *customOrderModel) QueryTotalOrders(ctx context.Context) (OrdersTotal, e
|
|||||||
return conn.Model(&Order{}).
|
return conn.Model(&Order{}).
|
||||||
Select(`
|
Select(`
|
||||||
SUM(amount) AS amount_total,
|
SUM(amount) AS amount_total,
|
||||||
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS new_order_amount,
|
SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
|
||||||
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS renewal_order_amount
|
SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
|
||||||
`).
|
`).
|
||||||
Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
|
Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
|
||||||
Scan(&result).Error
|
Scan(&result).Error
|
||||||
@@ -214,8 +214,8 @@ func (m *customOrderModel) QueryMonthlyUserCounts(ctx context.Context, date time
|
|||||||
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
|
||||||
return conn.Model(&Order{}).
|
return conn.Model(&Order{}).
|
||||||
Select(`
|
Select(`
|
||||||
COUNT(DISTINCT CASE WHEN type = 1 THEN user_id END) AS new_users,
|
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
|
||||||
COUNT(DISTINCT CASE WHEN type = 2 THEN user_id END) AS renewal_users
|
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
|
||||||
`).
|
`).
|
||||||
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
|
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
|
||||||
[]int64{2, 5}, firstDay, nextMonth, "balance").
|
[]int64{2, 5}, firstDay, nextMonth, "balance").
|
||||||
@@ -232,8 +232,8 @@ func (m *customOrderModel) QueryDateUserCounts(ctx context.Context, date time.Ti
|
|||||||
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
|
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
|
||||||
return conn.Model(&Order{}).
|
return conn.Model(&Order{}).
|
||||||
Select(`
|
Select(`
|
||||||
COUNT(DISTINCT CASE WHEN type = 1 THEN user_id END) AS new_users,
|
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
|
||||||
COUNT(DISTINCT CASE WHEN type = 2 THEN user_id END) AS renewal_users
|
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
|
||||||
`).
|
`).
|
||||||
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?",
|
Where("status IN ? AND DATE_FORMAT(created_at, '%Y-%m-%d') = ? AND method != ?",
|
||||||
[]int64{2, 5}, dateStr, "balance").
|
[]int64{2, 5}, dateStr, "balance").
|
||||||
@@ -249,8 +249,8 @@ func (m *customOrderModel) QueryTotalUserCounts(ctx context.Context) (int64, int
|
|||||||
return conn.Model(&Order{}).
|
return conn.Model(&Order{}).
|
||||||
Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
|
Where("status IN ? AND method != ?", []int64{2, 5}, "balance").
|
||||||
Select(`
|
Select(`
|
||||||
COUNT(DISTINCT CASE WHEN type = 1 THEN user_id END) AS new_users,
|
COUNT(DISTINCT CASE WHEN is_new = 1 THEN user_id END) AS new_users,
|
||||||
COUNT(DISTINCT CASE WHEN type = 2 THEN user_id END) AS renewal_users
|
COUNT(DISTINCT CASE WHEN is_new = 0 THEN user_id END) AS renewal_users
|
||||||
`).
|
`).
|
||||||
Scan(&counts).Error
|
Scan(&counts).Error
|
||||||
})
|
})
|
||||||
@@ -282,8 +282,8 @@ func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.T
|
|||||||
Select(`
|
Select(`
|
||||||
DATE_FORMAT(created_at, '%Y-%m-%d') AS date,
|
DATE_FORMAT(created_at, '%Y-%m-%d') AS date,
|
||||||
SUM(amount) AS amount_total,
|
SUM(amount) AS amount_total,
|
||||||
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS new_order_amount,
|
SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
|
||||||
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS renewal_order_amount
|
SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
|
||||||
`).
|
`).
|
||||||
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
|
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
|
||||||
[]int64{2, 5}, firstDay, nextDay, "balance").
|
[]int64{2, 5}, firstDay, nextDay, "balance").
|
||||||
@@ -326,8 +326,8 @@ func (m *customOrderModel) QueryMonthlyOrdersList(ctx context.Context, date time
|
|||||||
Select(`
|
Select(`
|
||||||
DATE_FORMAT(created_at, '%Y-%m') AS date,
|
DATE_FORMAT(created_at, '%Y-%m') AS date,
|
||||||
SUM(amount) AS amount_total,
|
SUM(amount) AS amount_total,
|
||||||
SUM(CASE WHEN type = 1 THEN amount ELSE 0 END) AS new_order_amount,
|
SUM(CASE WHEN is_new = 1 THEN amount ELSE 0 END) AS new_order_amount,
|
||||||
SUM(CASE WHEN type = 2 THEN amount ELSE 0 END) AS renewal_order_amount
|
SUM(CASE WHEN is_new = 0 THEN amount ELSE 0 END) AS renewal_order_amount
|
||||||
`).
|
`).
|
||||||
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
|
Where("status IN ? AND created_at >= ? AND created_at < ? AND method != ?",
|
||||||
[]int64{2, 5}, start, end, "balance").
|
[]int64{2, 5}, start, end, "balance").
|
||||||
|
|||||||
@@ -167,8 +167,11 @@ type Withdrawal struct {
|
|||||||
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
|
||||||
Amount int64 `gorm:"not null;comment:Withdrawal Amount"`
|
Amount int64 `gorm:"not null;comment:Withdrawal Amount"`
|
||||||
Content string `gorm:"type:text;comment:Withdrawal Content"`
|
Content string `gorm:"type:text;comment:Withdrawal Content"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected"`
|
Status uint8 `gorm:"type:tinyint(1);default:0;comment:Withdrawal Status: 0: Pending 1: Approved 2: Rejected 3: Cancelled"`
|
||||||
Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"`
|
Reason string `gorm:"type:varchar(500);default:'';comment:Rejection Reason"`
|
||||||
|
Method uint8 `gorm:"type:tinyint(1);default:0;comment:收款方式 0:其他 1:支付宝 2:微信 3:银行卡"`
|
||||||
|
Account string `gorm:"type:varchar(255);default:'';comment:收款账号"`
|
||||||
|
QrCodeUrl string `gorm:"type:varchar(500);default:'';comment:收款码图片URL"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
const (
|
||||||
|
WithdrawalStatusPending uint8 = 0 // 待审核
|
||||||
|
WithdrawalStatusApproved uint8 = 1 // 已通过
|
||||||
|
WithdrawalStatusRejected uint8 = 2 // 已拒绝
|
||||||
|
WithdrawalStatusCancelled uint8 = 3 // 已取消(用户自行撤回)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WithdrawalMethodOther uint8 = 0 // 其他
|
||||||
|
WithdrawalMethodAlipay uint8 = 1 // 支付宝
|
||||||
|
WithdrawalMethodWechat uint8 = 2 // 微信
|
||||||
|
WithdrawalMethodBank uint8 = 3 // 银行卡
|
||||||
|
)
|
||||||
+16
-5
@@ -298,6 +298,9 @@ type CommissionLog struct {
|
|||||||
type CommissionWithdrawRequest struct {
|
type CommissionWithdrawRequest struct {
|
||||||
Amount int64 `json:"amount"`
|
Amount int64 `json:"amount"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
|
Method uint8 `json:"method" validate:"oneof=0 1 2 3"`
|
||||||
|
Account string `json:"account,omitempty"`
|
||||||
|
QrCodeUrl string `json:"qr_code_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionRecords struct {
|
type ConnectionRecords struct {
|
||||||
@@ -2315,6 +2318,10 @@ type QueryUserSubscribeNodeListResponse struct {
|
|||||||
List []UserSubscribeInfo `json:"list"`
|
List []UserSubscribeInfo `json:"list"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CancelWithdrawalRequest struct {
|
||||||
|
WithdrawalId int64 `json:"withdrawal_id" validate:"required,gt=0"`
|
||||||
|
}
|
||||||
|
|
||||||
type QueryWithdrawalLogListRequest struct {
|
type QueryWithdrawalLogListRequest struct {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
@@ -2330,6 +2337,7 @@ type GetWithdrawalListRequest struct {
|
|||||||
Size int `form:"size"`
|
Size int `form:"size"`
|
||||||
UserId *int64 `form:"user_id,omitempty"`
|
UserId *int64 `form:"user_id,omitempty"`
|
||||||
Status *uint8 `form:"status,omitempty"`
|
Status *uint8 `form:"status,omitempty"`
|
||||||
|
Method *uint8 `form:"method,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetWithdrawalListResponse struct {
|
type GetWithdrawalListResponse struct {
|
||||||
@@ -3267,17 +3275,17 @@ type UpdateUserBasiceInfoRequest struct {
|
|||||||
UserId int64 `json:"user_id" validate:"required"`
|
UserId int64 `json:"user_id" validate:"required"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Balance int64 `json:"balance"`
|
Balance *int64 `json:"balance"`
|
||||||
Commission int64 `json:"commission"`
|
Commission *int64 `json:"commission"`
|
||||||
ReferralPercentage uint8 `json:"referral_percentage"`
|
ReferralPercentage uint8 `json:"referral_percentage"`
|
||||||
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
OnlyFirstPurchase *bool `json:"only_first_purchase"`
|
||||||
GiftAmount int64 `json:"gift_amount"`
|
GiftAmount *int64 `json:"gift_amount"`
|
||||||
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"`
|
||||||
Enable *bool `json:"enable"`
|
Enable *bool `json:"enable"`
|
||||||
IsAdmin *bool `json:"is_admin"`
|
IsAdmin *bool `json:"is_admin"`
|
||||||
Remark string `json:"remark"`
|
Remark *string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserNotifyRequest struct {
|
type UpdateUserNotifyRequest struct {
|
||||||
@@ -3648,6 +3656,9 @@ type WithdrawalLog struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Method uint8 `json:"method"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
QrCodeUrl string `json:"qr_code_url"`
|
||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUpdateUserBasicInfoRequestRemarkPresence(t *testing.T) {
|
||||||
|
var omitted UpdateUserBasiceInfoRequest
|
||||||
|
if err := json.Unmarshal([]byte(`{"user_id":1}`), &omitted); err != nil {
|
||||||
|
t.Fatalf("unmarshal omitted remark: %v", err)
|
||||||
|
}
|
||||||
|
if omitted.Remark != nil {
|
||||||
|
t.Fatalf("omitted remark = %q, want nil", *omitted.Remark)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cleared UpdateUserBasiceInfoRequest
|
||||||
|
if err := json.Unmarshal([]byte(`{"user_id":1,"remark":""}`), &cleared); err != nil {
|
||||||
|
t.Fatalf("unmarshal empty remark: %v", err)
|
||||||
|
}
|
||||||
|
if cleared.Remark == nil {
|
||||||
|
t.Fatal("empty remark was decoded as nil, want explicit empty string")
|
||||||
|
}
|
||||||
|
if *cleared.Remark != "" {
|
||||||
|
t.Fatalf("empty remark = %q, want empty string", *cleared.Remark)
|
||||||
|
}
|
||||||
|
|
||||||
|
var updated UpdateUserBasiceInfoRequest
|
||||||
|
if err := json.Unmarshal([]byte(`{"user_id":1,"remark":"new note"}`), &updated); err != nil {
|
||||||
|
t.Fatalf("unmarshal non-empty remark: %v", err)
|
||||||
|
}
|
||||||
|
if updated.Remark == nil || *updated.Remark != "new note" {
|
||||||
|
t.Fatalf("non-empty remark = %#v, want %q", updated.Remark, "new note")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user