Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ae9ad40de |
@@ -10,6 +10,12 @@ GRAFANA_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
|
|||||||
# PPanel Server 镜像标签(由 CI/CD 传入不可变 tag,如 git SHA)
|
# PPanel Server 镜像标签(由 CI/CD 传入不可变 tag,如 git SHA)
|
||||||
PPANEL_SERVER_TAG=CHANGE_ME_TO_GIT_SHA
|
PPANEL_SERVER_TAG=CHANGE_ME_TO_GIT_SHA
|
||||||
|
|
||||||
|
# 测试环境 MinIO / S3 兼容存储
|
||||||
|
# ppanel.yaml 中 S3.Endpoint 使用 http://127.0.0.1:9000,Bucket 与此处保持一致。
|
||||||
|
MINIO_ROOT_USER=CHANGE_ME_TO_S3_ACCESS_KEY
|
||||||
|
MINIO_ROOT_PASSWORD=CHANGE_ME_TO_S3_SECRET_KEY
|
||||||
|
MINIO_BUCKET=hifastvpn
|
||||||
|
|
||||||
# AWS 区域(香港)
|
# AWS 区域(香港)
|
||||||
AWS_REGION=ap-east-1
|
AWS_REGION=ap-east-1
|
||||||
|
|
||||||
|
|||||||
+33
-14
@@ -76,6 +76,29 @@ type (
|
|||||||
GiftAmount int64 `json:"gift_amount"`
|
GiftAmount int64 `json:"gift_amount"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
}
|
}
|
||||||
|
UserSubscribeDetail {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
UserId int64 `json:"user_id"`
|
||||||
|
User User `json:"user"`
|
||||||
|
OrderId int64 `json:"order_id"`
|
||||||
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
|
Subscribe Subscribe `json:"subscribe"`
|
||||||
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
|
GroupLocked bool `json:"group_locked"`
|
||||||
|
StartTime int64 `json:"start_time"`
|
||||||
|
ExpireTime int64 `json:"expire_time"`
|
||||||
|
ResetTime int64 `json:"reset_time"`
|
||||||
|
Traffic int64 `json:"traffic"`
|
||||||
|
Download int64 `json:"download"`
|
||||||
|
Upload int64 `json:"upload"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
EffectiveSpeed int64 `json:"effective_speed"`
|
||||||
|
IsThrottled bool `json:"is_throttled"`
|
||||||
|
ThrottleRule string `json:"throttle_rule,omitempty"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
}
|
||||||
BatchDeleteUserRequest {
|
BatchDeleteUserRequest {
|
||||||
Ids []int64 `json:"ids" validate:"required"`
|
Ids []int64 `json:"ids" validate:"required"`
|
||||||
}
|
}
|
||||||
@@ -135,22 +158,18 @@ type (
|
|||||||
Total int64 `json:"total"`
|
Total int64 `json:"total"`
|
||||||
}
|
}
|
||||||
CreateUserSubscribeRequest {
|
CreateUserSubscribeRequest {
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
ExpiredAt int64 `json:"expired_at"`
|
ExpiredAt int64 `json:"expired_at"`
|
||||||
Traffic int64 `json:"traffic"`
|
Traffic int64 `json:"traffic"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
SpeedLimit int64 `json:"speed_limit,optional"`
|
|
||||||
TrafficLimit string `json:"traffic_limit,optional"`
|
|
||||||
}
|
}
|
||||||
UpdateUserSubscribeRequest {
|
UpdateUserSubscribeRequest {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
Traffic int64 `json:"traffic"`
|
Traffic int64 `json:"traffic"`
|
||||||
ExpiredAt int64 `json:"expired_at"`
|
ExpiredAt int64 `json:"expired_at"`
|
||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Download int64 `json:"download"`
|
Download int64 `json:"download"`
|
||||||
SpeedLimit *int64 `json:"speed_limit,omitempty" validate:"omitempty,gte=0"`
|
|
||||||
TrafficLimit *string `json:"traffic_limit,omitempty"`
|
|
||||||
}
|
}
|
||||||
GetUserLoginLogsRequest {
|
GetUserLoginLogsRequest {
|
||||||
Page int `form:"page"`
|
Page int `form:"page"`
|
||||||
|
|||||||
+7
-36
@@ -171,13 +171,13 @@ type (
|
|||||||
DeviceLimit int64 `json:"device_limit"`
|
DeviceLimit int64 `json:"device_limit"`
|
||||||
}
|
}
|
||||||
VerifyConfig {
|
VerifyConfig {
|
||||||
CaptchaType string `json:"captcha_type"` // local or turnstile
|
CaptchaType string `json:"captcha_type"` // local or turnstile
|
||||||
TurnstileSiteKey string `json:"turnstile_site_key"`
|
TurnstileSiteKey string `json:"turnstile_site_key"`
|
||||||
TurnstileSecret string `json:"turnstile_secret"`
|
TurnstileSecret string `json:"turnstile_secret"`
|
||||||
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"` // User login captcha
|
EnableUserLoginCaptcha bool `json:"enable_user_login_captcha"` // User login captcha
|
||||||
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"` // User register captcha
|
EnableUserRegisterCaptcha bool `json:"enable_user_register_captcha"` // User register captcha
|
||||||
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"` // Admin login captcha
|
EnableAdminLoginCaptcha bool `json:"enable_admin_login_captcha"` // Admin login captcha
|
||||||
EnableUserResetPasswordCaptcha bool `json:"enable_user_reset_password_captcha"` // User reset password captcha
|
EnableUserResetPasswordCaptcha bool `json:"enable_user_reset_password_captcha"` // User reset password captcha
|
||||||
}
|
}
|
||||||
NodeConfig {
|
NodeConfig {
|
||||||
NodeSecret string `json:"node_secret"`
|
NodeSecret string `json:"node_secret"`
|
||||||
@@ -536,35 +536,6 @@ type (
|
|||||||
CreatedAt int64 `json:"created_at"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
}
|
}
|
||||||
UserSubscribeDetail {
|
|
||||||
Id int64 `json:"id"`
|
|
||||||
UserId int64 `json:"user_id"`
|
|
||||||
User User `json:"user"`
|
|
||||||
OrderId int64 `json:"order_id"`
|
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
|
||||||
Subscribe Subscribe `json:"subscribe"`
|
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
|
||||||
NodeGroupName string `json:"node_group_name"`
|
|
||||||
GroupLocked bool `json:"group_locked"`
|
|
||||||
StartTime int64 `json:"start_time"`
|
|
||||||
ExpireTime int64 `json:"expire_time"`
|
|
||||||
ResetTime int64 `json:"reset_time"`
|
|
||||||
Traffic int64 `json:"traffic"`
|
|
||||||
Download int64 `json:"download"`
|
|
||||||
Upload int64 `json:"upload"`
|
|
||||||
SpeedLimit int64 `json:"speed_limit"`
|
|
||||||
TrafficLimit []TrafficLimit `json:"user_traffic_limit"`
|
|
||||||
PlanSpeedLimit int64 `json:"plan_speed_limit"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
Status uint8 `json:"status"`
|
|
||||||
EffectiveSpeed int64 `json:"effective_speed"`
|
|
||||||
IsThrottled bool `json:"is_throttled"`
|
|
||||||
ThrottleRule string `json:"throttle_rule,omitempty"`
|
|
||||||
ThrottleStart int64 `json:"throttle_start,omitempty"`
|
|
||||||
ThrottleEnd int64 `json:"throttle_end,omitempty"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
|
||||||
UserAffiliate {
|
UserAffiliate {
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
tempo:
|
tempo:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
@@ -239,6 +241,63 @@ services:
|
|||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# 10. MinIO (测试环境 S3 兼容对象存储)
|
||||||
|
# 仅绑定宿主机 127.0.0.1,供 host 网络的 ppanel-server 访问,不对公网开放。
|
||||||
|
# ----------------------------------------------------
|
||||||
|
minio:
|
||||||
|
image: minio/minio:RELEASE.2025-05-24T17-08-30Z-cpuv1
|
||||||
|
container_name: ppanel-minio
|
||||||
|
restart: always
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: "${MINIO_ROOT_USER:?请在 .env 文件中设置 MINIO_ROOT_USER}"
|
||||||
|
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?请在 .env 文件中设置 MINIO_ROOT_PASSWORD}"
|
||||||
|
TZ: Asia/Shanghai
|
||||||
|
volumes:
|
||||||
|
- ./data/minio:/data
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:9000:9000"
|
||||||
|
- "127.0.0.1:9001:9001"
|
||||||
|
networks:
|
||||||
|
- ppanel_net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 6
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc:RELEASE.2025-07-21T05-28-08Z-cpuv1
|
||||||
|
container_name: ppanel-minio-init
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: "${MINIO_ROOT_USER:?请在 .env 文件中设置 MINIO_ROOT_USER}"
|
||||||
|
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?请在 .env 文件中设置 MINIO_ROOT_PASSWORD}"
|
||||||
|
MINIO_BUCKET: "${MINIO_BUCKET:?请在 .env 文件中设置 MINIO_BUCKET}"
|
||||||
|
entrypoint:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
mc alias set local http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"
|
||||||
|
mc mb --ignore-existing "local/$${MINIO_BUCKET}"
|
||||||
|
networks:
|
||||||
|
- ppanel_net
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
loki_data:
|
loki_data:
|
||||||
grafana_data:
|
grafana_data:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
-- Purpose: Rollback user-level speed limit overrides from user_subscribe
|
|
||||||
|
|
||||||
ALTER TABLE `user_subscribe`
|
|
||||||
DROP COLUMN IF EXISTS `traffic_limit`,
|
|
||||||
DROP COLUMN IF EXISTS `speed_limit`;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
-- Purpose: Add user-level speed limit overrides to user_subscribe
|
|
||||||
|
|
||||||
SET @column_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'user_subscribe'
|
|
||||||
AND COLUMN_NAME = 'speed_limit'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@column_exists = 0,
|
|
||||||
'ALTER TABLE `user_subscribe` ADD COLUMN `speed_limit` int NOT NULL DEFAULT 0 COMMENT ''User-level speed limit override (Mbps, 0=use plan default)'' AFTER `upload`',
|
|
||||||
'SELECT ''Column speed_limit already exists in user_subscribe table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
SET @column_exists = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'user_subscribe'
|
|
||||||
AND COLUMN_NAME = 'traffic_limit'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET @sql = IF(
|
|
||||||
@column_exists = 0,
|
|
||||||
'ALTER TABLE `user_subscribe` ADD COLUMN `traffic_limit` text DEFAULT NULL COMMENT ''User-level traffic limit rules override (JSON, NULL=use plan default)'' AFTER `speed_limit`',
|
|
||||||
'SELECT ''Column traffic_limit already exists in user_subscribe table'''
|
|
||||||
);
|
|
||||||
|
|
||||||
PREPARE stmt FROM @sql;
|
|
||||||
EXECUTE stmt;
|
|
||||||
DEALLOCATE PREPARE stmt;
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/perfect-panel/server/internal/logic/admin/user"
|
"github.com/perfect-panel/server/internal/logic/admin/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
@@ -21,25 +18,9 @@ func UpdateUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context)
|
|||||||
result.ParamErrorResult(c, validateErr)
|
result.ParamErrorResult(c, validateErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := validateUpdateUserSubscribeTrafficLimit(&req); err != nil {
|
|
||||||
result.ParamErrorResult(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l := user.NewUpdateUserSubscribeLogic(c.Request.Context(), svcCtx)
|
l := user.NewUpdateUserSubscribeLogic(c.Request.Context(), svcCtx)
|
||||||
err := l.UpdateUserSubscribe(&req)
|
err := l.UpdateUserSubscribe(&req)
|
||||||
result.HttpResult(c, nil, err)
|
result.HttpResult(c, nil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUpdateUserSubscribeTrafficLimit(req *types.UpdateUserSubscribeRequest) error {
|
|
||||||
if req.TrafficLimit == nil || *req.TrafficLimit == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var rules []types.TrafficLimit
|
|
||||||
if err := json.Unmarshal([]byte(*req.TrafficLimit), &rules); err != nil {
|
|
||||||
return errors.New("traffic_limit must be a valid JSON array")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
package user
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUpdateUserSubscribeHandlerRejectsInvalidLimits(t *testing.T) {
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
body string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "negative speed limit",
|
|
||||||
body: `{"user_subscribe_id":1,"subscribe_id":1,"traffic":0,"expired_at":4102444800000,"upload":0,"download":0,"speed_limit":-1}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid traffic limit json",
|
|
||||||
body: `{"user_subscribe_id":1,"subscribe_id":1,"traffic":0,"expired_at":4102444800000,"upload":0,"download":0,"traffic_limit":"not-json"}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
router := gin.New()
|
|
||||||
router.PUT("/v1/admin/user/subscribe", UpdateUserSubscribeHandler(&svc.ServiceContext{}))
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/v1/admin/user/subscribe", bytes.NewBufferString(tt.body))
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
router.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("expected HTTP 200, got %d", rec.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp struct {
|
|
||||||
Code uint32 `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
||||||
t.Fatalf("unmarshal response: %v", err)
|
|
||||||
}
|
|
||||||
if resp.Code != xerr.InvalidParams {
|
|
||||||
t.Fatalf("expected code %d, got %d (%s)", xerr.InvalidParams, resp.Code, resp.Msg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -39,32 +39,22 @@ func (l *UpdateUserSubscribeLogic) UpdateUserSubscribe(req *types.UpdateUserSubs
|
|||||||
} else {
|
} else {
|
||||||
userSub.Status = 1
|
userSub.Status = 1
|
||||||
}
|
}
|
||||||
speedLimit := userSub.SpeedLimit
|
|
||||||
if req.SpeedLimit != nil {
|
|
||||||
speedLimit = *req.SpeedLimit
|
|
||||||
}
|
|
||||||
trafficLimit := userSub.TrafficLimit
|
|
||||||
if req.TrafficLimit != nil {
|
|
||||||
trafficLimit = *req.TrafficLimit
|
|
||||||
}
|
|
||||||
|
|
||||||
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{
|
err = l.svcCtx.UserModel.UpdateSubscribe(l.ctx, &user.Subscribe{
|
||||||
Id: userSub.Id,
|
Id: userSub.Id,
|
||||||
UserId: userSub.UserId,
|
UserId: userSub.UserId,
|
||||||
OrderId: userSub.OrderId,
|
OrderId: userSub.OrderId,
|
||||||
SubscribeId: req.SubscribeId,
|
SubscribeId: req.SubscribeId,
|
||||||
StartTime: userSub.StartTime,
|
StartTime: userSub.StartTime,
|
||||||
ExpireTime: time.UnixMilli(req.ExpiredAt),
|
ExpireTime: time.UnixMilli(req.ExpiredAt),
|
||||||
Traffic: req.Traffic,
|
Traffic: req.Traffic,
|
||||||
Download: req.Download,
|
Download: req.Download,
|
||||||
Upload: req.Upload,
|
Upload: req.Upload,
|
||||||
SpeedLimit: speedLimit,
|
Token: userSub.Token,
|
||||||
TrafficLimit: trafficLimit,
|
UUID: userSub.UUID,
|
||||||
Token: userSub.Token,
|
Status: userSub.Status,
|
||||||
UUID: userSub.UUID,
|
NodeGroupId: userSub.NodeGroupId,
|
||||||
Status: userSub.Status,
|
GroupLocked: userSub.GroupLocked,
|
||||||
NodeGroupId: userSub.NodeGroupId,
|
|
||||||
GroupLocked: userSub.GroupLocked,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -101,8 +101,6 @@ type Subscribe struct {
|
|||||||
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
Traffic int64 `gorm:"default:0;comment:Traffic"`
|
||||||
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
Download int64 `gorm:"default:0;comment:Download Traffic"`
|
||||||
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
Upload int64 `gorm:"default:0;comment:Upload Traffic"`
|
||||||
SpeedLimit int64 `gorm:"default:0;comment:User-level speed limit override (Mbps, 0=use plan default)"`
|
|
||||||
TrafficLimit string `gorm:"type:text;default:null;comment:User-level traffic limit rules override (JSON)"`
|
|
||||||
ExpiredDownload int64 `gorm:"default:0;comment:Expired period download traffic (bytes)"`
|
ExpiredDownload int64 `gorm:"default:0;comment:Expired period download traffic (bytes)"`
|
||||||
ExpiredUpload int64 `gorm:"default:0;comment:Expired period upload traffic (bytes)"`
|
ExpiredUpload int64 `gorm:"default:0;comment:Expired period upload traffic (bytes)"`
|
||||||
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
Token string `gorm:"index:idx_token;unique;type:varchar(255);default:'';comment:Token"`
|
||||||
|
|||||||
+34
-41
@@ -541,12 +541,10 @@ type CreateUserRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateUserSubscribeRequest struct {
|
type CreateUserSubscribeRequest struct {
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
ExpiredAt int64 `json:"expired_at"`
|
ExpiredAt int64 `json:"expired_at"`
|
||||||
Traffic int64 `json:"traffic"`
|
Traffic int64 `json:"traffic"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
SpeedLimit int64 `json:"speed_limit,optional"`
|
|
||||||
TrafficLimit string `json:"traffic_limit,optional"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateUserTicketFollowRequest struct {
|
type CreateUserTicketFollowRequest struct {
|
||||||
@@ -3319,14 +3317,12 @@ type UpdateUserSubscribeNoteRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserSubscribeRequest struct {
|
type UpdateUserSubscribeRequest struct {
|
||||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
Traffic int64 `json:"traffic"`
|
Traffic int64 `json:"traffic"`
|
||||||
ExpiredAt int64 `json:"expired_at"`
|
ExpiredAt int64 `json:"expired_at"`
|
||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
Download int64 `json:"download"`
|
Download int64 `json:"download"`
|
||||||
SpeedLimit *int64 `json:"speed_limit,omitempty" validate:"omitempty,gte=0"`
|
|
||||||
TrafficLimit *string `json:"traffic_limit,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserTicketStatusRequest struct {
|
type UpdateUserTicketStatusRequest struct {
|
||||||
@@ -3478,33 +3474,30 @@ type UserSubscribe struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UserSubscribeDetail struct {
|
type UserSubscribeDetail struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
User User `json:"user"`
|
User User `json:"user"`
|
||||||
OrderId int64 `json:"order_id"`
|
OrderId int64 `json:"order_id"`
|
||||||
SubscribeId int64 `json:"subscribe_id"`
|
SubscribeId int64 `json:"subscribe_id"`
|
||||||
Subscribe Subscribe `json:"subscribe"`
|
Subscribe Subscribe `json:"subscribe"`
|
||||||
NodeGroupId int64 `json:"node_group_id"`
|
NodeGroupId int64 `json:"node_group_id"`
|
||||||
NodeGroupName string `json:"node_group_name"`
|
NodeGroupName string `json:"node_group_name"`
|
||||||
GroupLocked bool `json:"group_locked"`
|
GroupLocked bool `json:"group_locked"`
|
||||||
StartTime int64 `json:"start_time"`
|
StartTime int64 `json:"start_time"`
|
||||||
ExpireTime int64 `json:"expire_time"`
|
ExpireTime int64 `json:"expire_time"`
|
||||||
ResetTime int64 `json:"reset_time"`
|
ResetTime int64 `json:"reset_time"`
|
||||||
Traffic int64 `json:"traffic"`
|
Traffic int64 `json:"traffic"`
|
||||||
Download int64 `json:"download"`
|
Download int64 `json:"download"`
|
||||||
Upload int64 `json:"upload"`
|
Upload int64 `json:"upload"`
|
||||||
SpeedLimit int64 `json:"speed_limit"`
|
Token string `json:"token"`
|
||||||
TrafficLimit []TrafficLimit `json:"user_traffic_limit"`
|
Status uint8 `json:"status"`
|
||||||
PlanSpeedLimit int64 `json:"plan_speed_limit"`
|
EffectiveSpeed int64 `json:"effective_speed"`
|
||||||
Token string `json:"token"`
|
IsThrottled bool `json:"is_throttled"`
|
||||||
Status uint8 `json:"status"`
|
ThrottleRule string `json:"throttle_rule,omitempty"`
|
||||||
EffectiveSpeed int64 `json:"effective_speed"`
|
ThrottleStart int64 `json:"throttle_start,omitempty"`
|
||||||
IsThrottled bool `json:"is_throttled"`
|
ThrottleEnd int64 `json:"throttle_end,omitempty"`
|
||||||
ThrottleRule string `json:"throttle_rule,omitempty"`
|
CreatedAt int64 `json:"created_at"`
|
||||||
ThrottleStart int64 `json:"throttle_start,omitempty"`
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
ThrottleEnd int64 `json:"throttle_end,omitempty"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
UpdatedAt int64 `json:"updated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserSubscribeInfo struct {
|
type UserSubscribeInfo struct {
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# HiFast 上传链路运维 Runbook
|
||||||
|
|
||||||
|
## 适用范围
|
||||||
|
|
||||||
|
本文档用于测试环境 `https://tapi.hifast.biz/v1/public/file/upload` 上传链路自检和 MinIO sidecar 运维。测试环境的对象存储应自包含在 compose 栈内,不依赖外部 `107.173.50.22:5017`。
|
||||||
|
|
||||||
|
## 当前测试环境配置
|
||||||
|
|
||||||
|
- Compose 文件:`/root/bindbox/docker-compose.cloud.yml`
|
||||||
|
- 应用配置:`/root/bindbox/configs/ppanel.yaml`
|
||||||
|
- MinIO 数据目录:`/root/bindbox/data/minio`
|
||||||
|
- MinIO API:`http://127.0.0.1:9000`
|
||||||
|
- MinIO Console:`http://127.0.0.1:9001`,仅绑定本机,需 SSH 隧道访问
|
||||||
|
- Bucket:与 `ppanel.yaml` 的 `S3.Bucket` 保持一致,当前为 `hifastvpn`
|
||||||
|
|
||||||
|
`ppanel-server` 使用 host 网络,所以 `S3.Endpoint` 必须写宿主机 loopback:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
S3:
|
||||||
|
Enable: true
|
||||||
|
Endpoint: "http://127.0.0.1:9000"
|
||||||
|
Bucket: "hifastvpn"
|
||||||
|
UsePathStyle: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启停与状态检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/bindbox
|
||||||
|
docker compose -f docker-compose.cloud.yml up -d minio minio-init
|
||||||
|
docker restart ppanel-server
|
||||||
|
|
||||||
|
docker compose -f docker-compose.cloud.yml ps minio minio-init ppanel-server
|
||||||
|
curl -fsS http://127.0.0.1:9000/minio/health/live
|
||||||
|
curl -fsS https://tapi.hifast.biz/v1/common/heartbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
`minio-init` 是一次性 bucket 初始化容器,正常状态是 `exited` 且退出码为 `0`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker inspect ppanel-minio-init --format 'status={{.State.Status}} exit={{.State.ExitCode}}'
|
||||||
|
docker logs --tail 50 ppanel-minio-init
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bucket 重建
|
||||||
|
|
||||||
|
如 bucket 被误删或新环境首次初始化失败:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/bindbox
|
||||||
|
docker compose -f docker-compose.cloud.yml up -d minio
|
||||||
|
docker compose -f docker-compose.cloud.yml run --rm minio-init
|
||||||
|
```
|
||||||
|
|
||||||
|
确认 bucket:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs --tail 50 ppanel-minio-init
|
||||||
|
find /root/bindbox/data/minio -maxdepth 3 -type d | sort | head
|
||||||
|
```
|
||||||
|
|
||||||
|
## 上传链路自检
|
||||||
|
|
||||||
|
基础健康检查:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i https://tapi.hifast.biz/v1/common/heartbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
完整上传需要有效用户 JWT。复测标准:
|
||||||
|
|
||||||
|
- `POST /v1/public/file/upload` 返回 HTTP 200
|
||||||
|
- 响应业务 `code=200`
|
||||||
|
- `data.url` 指向 `S3.PublicBaseURL + object_key`
|
||||||
|
- MinIO 数据目录可看到对应对象
|
||||||
|
- `ppanel-server` 日志没有 `put object failed` 或 `context canceled`
|
||||||
|
|
||||||
|
排查命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs --since 10m ppanel-server | grep -Ei 'file/upload|put object|context canceled|s3'
|
||||||
|
find /root/bindbox/data/minio/hifastvpn -type f | sort | tail
|
||||||
|
```
|
||||||
|
|
||||||
|
## 备份
|
||||||
|
|
||||||
|
变更前必须备份:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/bindbox
|
||||||
|
ts=$(date +%Y%m%d%H%M%S)
|
||||||
|
cp docker-compose.cloud.yml docker-compose.cloud.yml.bak.hif-101.$ts
|
||||||
|
cp .env .env.bak.hif-101.$ts
|
||||||
|
cp configs/ppanel.yaml configs/ppanel.yaml.bak.$ts
|
||||||
|
```
|
||||||
|
|
||||||
|
MinIO 数据备份:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/bindbox
|
||||||
|
tar -czf minio-data.$(date +%Y%m%d%H%M%S).tar.gz data/minio
|
||||||
|
```
|
||||||
|
|
||||||
|
## 回滚
|
||||||
|
|
||||||
|
恢复外部 S3 配置或回到变更前状态:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/bindbox
|
||||||
|
cp configs/ppanel.yaml.bak.YYYYMMDDHHMMSS configs/ppanel.yaml
|
||||||
|
cp docker-compose.cloud.yml.bak.hif-101.YYYYMMDDHHMMSS docker-compose.cloud.yml
|
||||||
|
cp .env.bak.hif-101.YYYYMMDDHHMMSS .env
|
||||||
|
docker compose -f docker-compose.cloud.yml stop minio
|
||||||
|
docker restart ppanel-server
|
||||||
|
```
|
||||||
|
|
||||||
|
保留 `/root/bindbox/data/minio` 便于复盘,不要在回滚时删除数据目录。
|
||||||
|
|
||||||
|
## 旧 Endpoint 清理
|
||||||
|
|
||||||
|
测试栈活动配置不得继续引用旧外部 endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/bindbox
|
||||||
|
grep -R '107\.173\.50\.22:5017' -n docker-compose.cloud.yml configs/ppanel.yaml .env || true
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user