Compare commits

..

4 Commits

Author SHA1 Message Date
eb414749c3 fix: apiversion.UseLatest 阈值判断改为 >= 避免误判新版本为老版本
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m26s
原 compare > 0 要求严格大于阈值,导致 header=1.0.0 被判为老版本,
去掉最后一个套餐后列表为空。改为 >= 0,有 header 且版本 >= 阈值均视为新版本。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-28 20:42:57 -07:00
1a08949c80 fix: 流量触发限速后主动清除节点用户缓存
节点用户列表缓存(server:user:{id})永不过期,用户流量超限触发按量限速后
缓存中仍是旧的速度值,节点不会感知限速状态。

修复:每次写入 traffic_log 后,检查该订阅是否触发按量限速规则,
若 IsThrottled=true 则立即删除对应节点的用户列表缓存,
节点下次拉取时重新计算并应用降速后的 speed_limit。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-28 18:36:13 -07:00
0f28f4995f fix: 修复在线设备列表无数据问题
user_device 表的 subscribe_id 字段从未在插入时写入(始终为 NULL),
导致 QueryDevicePageList 的 WHERE 条件 `subscribe_id = ?` 永远匹配不到记录。
移除 subscribe_id 过滤,改为只按 user_id 查询。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-28 18:23:18 -07:00
8398865bd3 fix: SingleModel 下首购 isNew 判断错误导致佣金不发
SingleModel 模式下,用户首次购买会被路由成 orderType=2(续费),
导致 isNew 判断逻辑跳过,始终为 false,激活时 shouldProcessCommission
误判为非首购,佣金不发给邀请人。

修复:去除 isNew 查询对 orderType==1 的依赖,始终通过
IsUserEligibleForNewOrder 判断用户是否有历史完成订单。

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-28 11:28:02 -07:00
4 changed files with 24 additions and 9 deletions

View File

@ -236,14 +236,12 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
} }
} }
// query user is new purchase or renewal // query user is new purchase or renewal
isNew := false // 注意SingleModel 下 orderType 会被路由成 2续费但仍需正确判断是否首购以发佣金
if orderType == 1 { isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
isNew, err = l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil { if err != nil {
l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) l.Errorw("[Purchase] Database query error", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user order error: %v", err.Error())
} }
}
// create order // create order
orderInfo := &order.Order{ orderInfo := &order.Order{
UserId: u.Id, UserId: u.Id,

View File

@ -41,7 +41,7 @@ func (m *customUserModel) QueryDevicePageList(ctx context.Context, userId, subsc
var list []*Device var list []*Device
var total int64 var total int64
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error { err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Device{}).Where("`user_id` = ? and `subscribe_id` = ?", userId, subscribeId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error return conn.Model(&Device{}).Where("`user_id` = ?", userId).Count(&total).Limit(size).Offset((page - 1) * size).Find(&list).Error
}) })
return list, total, err return list, total, err
} }

View File

@ -54,7 +54,7 @@ func UseLatest(header string, threshold string) bool {
thresholdVersion, _ = Parse(DefaultThreshold) thresholdVersion, _ = Parse(DefaultThreshold)
} }
return compare(currentVersion, thresholdVersion) > 0 return compare(currentVersion, thresholdVersion) >= 0
} }
func compare(left Version, right Version) int { func compare(left Version, right Version) int {

View File

@ -3,11 +3,13 @@ package traffic
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"time" "time"
"github.com/perfect-panel/server/internal/model/node" "github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/speedlimit"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/traffic" "github.com/perfect-panel/server/internal/model/traffic"
@ -126,6 +128,21 @@ func (l *TrafficStatisticsLogic) ProcessTask(ctx context.Context, task *asynq.Ta
logger.Field("error", err.Error()), logger.Field("error", err.Error()),
) )
} }
// 写完流量后检查是否触发按量限速,若触发则清除节点缓存使限速立即生效
if planSub, planErr := l.svc.SubscribeModel.FindOne(ctx, sub.SubscribeId); planErr == nil &&
(planSub.SpeedLimit > 0 || planSub.TrafficLimit != "") {
throttle := speedlimit.Calculate(ctx, l.svc.DB, sub.UserId, sub.Id, planSub.SpeedLimit, planSub.TrafficLimit)
if throttle.IsThrottled {
cacheKey := fmt.Sprintf("%s%d", node.ServerUserListCacheKey, payload.ServerId)
if delErr := l.svc.Redis.Del(ctx, cacheKey).Err(); delErr != nil {
logger.WithContext(ctx).Error("[TrafficStatistics] Clear server user cache failed",
logger.Field("serverId", payload.ServerId),
logger.Field("error", delErr.Error()),
)
}
}
}
} }
return nil return nil
} }