邀请
This commit is contained in:
@@ -207,7 +207,7 @@ type InviteConfig struct {
|
||||
ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
|
||||
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
|
||||
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
|
||||
GiftDays int64 `yaml:"GiftDays" default:"0"`
|
||||
GiftDays int64 `yaml:"GiftDays" default:"3"`
|
||||
}
|
||||
|
||||
// KuttConfig Kutt 短链接服务配置
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -31,6 +32,9 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// 统一处理邮箱格式:转小写并去空格,与发送验证码逻辑保持一致
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
|
||||
// 校验邮箱验证码
|
||||
if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -52,6 +52,10 @@ func (l *DeleteUserDeviceLogic) DeleteUserDevice(req *types.DeleteUserDeivceRequ
|
||||
_ = l.svcCtx.Redis.Del(ctx, sessionIdCacheKey).Err()
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, device.UserId)
|
||||
_ = l.svcCtx.Redis.ZRem(ctx, sessionsKey, sessionId).Err()
|
||||
l.Infow("[SessionMonitor] 管理员删除设备触发 Session 清理",
|
||||
logger.Field("user_id", device.UserId),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("device_id", device.Id))
|
||||
}
|
||||
|
||||
// 使用事务同时删除设备记录和关联的认证方式
|
||||
|
||||
@@ -2,6 +2,9 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@@ -63,8 +66,9 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
|
||||
// Set MemberStatus and update LastLoginTime from traffic
|
||||
if info, ok := activeSubs[item.Id]; ok {
|
||||
u.MemberStatus = info.MemberStatus
|
||||
|
||||
days := math.Ceil(info.ExpireTime.Sub(time.Now()).Hours() / 24)
|
||||
u.MemberStatus = fmt.Sprintf("%s*%d", info.MemberStatus, int(days))
|
||||
|
||||
if info.LastTrafficAt != nil {
|
||||
trafficTime := info.LastTrafficAt.Unix()
|
||||
if trafficTime > u.LastLoginTime {
|
||||
|
||||
@@ -2,6 +2,7 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -35,6 +36,14 @@ func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Devic
|
||||
}
|
||||
|
||||
func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) {
|
||||
// 打印请求参数
|
||||
l.Infow("DeviceLogin 请求参数",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("ip", req.IP),
|
||||
logger.Field("user_agent", req.UserAgent),
|
||||
logger.Field("cf_token", req.CfToken),
|
||||
)
|
||||
|
||||
if !l.svcCtx.Config.Device.Enable {
|
||||
return nil, xerr.NewErrMsg("Device login is disabled")
|
||||
}
|
||||
@@ -97,6 +106,18 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
}
|
||||
}
|
||||
|
||||
// [AuthDebug] Log detailed User Info
|
||||
if userInfo != nil {
|
||||
l.Infow("[AuthDebug] User Info Loaded",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("enable", userInfo.Enable),
|
||||
logger.Field("is_admin", userInfo.IsAdmin),
|
||||
logger.Field("balance", userInfo.Balance),
|
||||
logger.Field("member_status", userInfo.MemberStatus),
|
||||
logger.Field("created_at", userInfo.CreatedAt),
|
||||
)
|
||||
}
|
||||
|
||||
if createdNewDevice {
|
||||
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
|
||||
if err != nil {
|
||||
@@ -118,24 +139,55 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// [AuthDebug] Log detailed Device Info
|
||||
l.Infow("[AuthDebug] Device Info",
|
||||
logger.Field("device_id", deviceInfo.Id),
|
||||
logger.Field("identifier", deviceInfo.Identifier),
|
||||
logger.Field("ip", deviceInfo.Ip),
|
||||
logger.Field("enabled", deviceInfo.Enabled),
|
||||
logger.Field("online", deviceInfo.Online),
|
||||
logger.Field("user_agent", deviceInfo.UserAgent),
|
||||
)
|
||||
}
|
||||
|
||||
// Check if device has an existing valid session - reuse it instead of creating new one
|
||||
var sessionId string
|
||||
var reuseSession bool
|
||||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
|
||||
l.Infow("[SESSION_DEBUG] logic start: checking device cache",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("device_cache_key", deviceCacheKey),
|
||||
)
|
||||
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && oldSid != "" {
|
||||
l.Infow("[SESSION_DEBUG] device cache hit",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("old_session_id", oldSid),
|
||||
)
|
||||
|
||||
// Check if old session is still valid AND belongs to current user
|
||||
oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid)
|
||||
if uidStr, existErr := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); existErr == nil && uidStr != "" {
|
||||
l.Infow("[SESSION_DEBUG] session cache hit",
|
||||
logger.Field("old_session_id", oldSid),
|
||||
logger.Field("session_user_id", uidStr),
|
||||
logger.Field("current_user_id", userInfo.Id),
|
||||
)
|
||||
|
||||
// Verify session belongs to current user (防止设备转移后复用其他用户的session)
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
// Check TTL
|
||||
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
|
||||
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("session_ttl", ttl.Seconds()),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
@@ -144,7 +196,17 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
logger.Field("identifier", req.Identifier),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
l.Infow("[SESSION_DEBUG] session cache miss or invalid",
|
||||
logger.Field("old_session_id", oldSid),
|
||||
logger.Field("error", existErr),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
l.Infow("[SESSION_DEBUG] device cache miss",
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("error", getErr),
|
||||
)
|
||||
}
|
||||
if !reuseSession {
|
||||
sessionId = uuidx.NewUUID().String()
|
||||
@@ -156,10 +218,21 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
}
|
||||
|
||||
// Generate token (always generate new token, but may reuse sessionId)
|
||||
nowTime := time.Now().Unix()
|
||||
accessExpire := l.svcCtx.Config.JwtAuth.AccessExpire
|
||||
|
||||
l.Infow("[AuthDebug] Generating Token",
|
||||
logger.Field("iat", nowTime),
|
||||
logger.Field("expire_seconds", accessExpire),
|
||||
logger.Field("calculated_exp", nowTime+accessExpire),
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("session_id", sessionId),
|
||||
)
|
||||
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
nowTime,
|
||||
accessExpire,
|
||||
jwt.WithOption("UserId", userInfo.Id),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", "device"),
|
||||
@@ -199,6 +272,37 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set device id error: %v", err.Error())
|
||||
}
|
||||
|
||||
// [Debug] Store session detail for troubleshooting
|
||||
// This helps us see "who is online" when multiple devices are logged in.
|
||||
sessionDetail := map[string]interface{}{
|
||||
"session_id": sessionId,
|
||||
"user_id": userInfo.Id,
|
||||
"identifier": req.Identifier,
|
||||
"ip": req.IP,
|
||||
"user_agent": req.UserAgent,
|
||||
"login_time": time.Now().Format("2006-01-02 15:04:05"),
|
||||
"device_id": deviceInfo.Id,
|
||||
}
|
||||
if detailJson, err := json.Marshal(sessionDetail); err == nil {
|
||||
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionId)
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, detailKey, string(detailJson), time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
|
||||
// 登录成功 - 打印详细信息用于调试
|
||||
l.Infow("========== 设备登录成功 ==========",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("device_id", deviceInfo.Id),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("reuse_session", reuseSession),
|
||||
logger.Field("login_type", "device"),
|
||||
logger.Field("login_ip", req.IP),
|
||||
logger.Field("user_agent", req.UserAgent),
|
||||
logger.Field("session_limit", l.svcCtx.SessionLimit()),
|
||||
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
|
||||
logger.Field("devices_count", len(userInfo.UserDevices)),
|
||||
)
|
||||
|
||||
loginStatus = true
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
|
||||
@@ -39,6 +39,18 @@ func NewEmailLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *EmailL
|
||||
}
|
||||
|
||||
func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.LoginResponse, err error) {
|
||||
// 打印请求参数
|
||||
l.Infow("EmailLogin 请求参数",
|
||||
logger.Field("email", req.Email),
|
||||
logger.Field("code", req.Code),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("invite", req.Invite),
|
||||
logger.Field("ip", req.IP),
|
||||
logger.Field("user_agent", req.UserAgent),
|
||||
logger.Field("login_type", req.LoginType),
|
||||
logger.Field("cf_token", req.CfToken),
|
||||
)
|
||||
|
||||
loginStatus := false
|
||||
var userInfo *user.User
|
||||
var isNewUser bool
|
||||
@@ -234,10 +246,14 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
// Check TTL
|
||||
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
|
||||
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("session_ttl", ttl.Seconds()),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
@@ -285,6 +301,21 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
|
||||
// 登录成功 - 打印详细信息用于调试
|
||||
l.Infow("========== 邮箱登录成功 ==========",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("device_id", deviceId),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("reuse_session", reuseSession),
|
||||
logger.Field("login_type", req.LoginType),
|
||||
logger.Field("login_ip", req.IP),
|
||||
logger.Field("user_agent", req.UserAgent),
|
||||
logger.Field("session_limit", l.svcCtx.SessionLimit()),
|
||||
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
|
||||
logger.Field("devices_count", len(userInfo.UserDevices)),
|
||||
)
|
||||
|
||||
loginStatus = true
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
|
||||
@@ -40,6 +40,18 @@ func NewTelephoneLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Te
|
||||
}
|
||||
|
||||
func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r *http.Request, ip string) (resp *types.LoginResponse, err error) {
|
||||
// 打印请求参数 (隐藏密码)
|
||||
l.Infow("TelephoneLogin 请求参数",
|
||||
logger.Field("telephone_area_code", req.TelephoneAreaCode),
|
||||
logger.Field("telephone", req.Telephone),
|
||||
logger.Field("has_password", req.Password != ""),
|
||||
logger.Field("has_code", req.TelephoneCode != ""),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("ip", ip),
|
||||
logger.Field("user_agent", r.UserAgent()),
|
||||
logger.Field("login_type", req.LoginType),
|
||||
)
|
||||
|
||||
phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
|
||||
@@ -158,10 +170,14 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
// Check TTL
|
||||
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
|
||||
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("session_ttl", ttl.Seconds()),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
@@ -209,6 +225,21 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
loginStatus = true
|
||||
// 登录成功 - 打印详细信息用于调试
|
||||
l.Infow("========== 手机登录成功 ==========",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("telephone", phoneNumber),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("reuse_session", reuseSession),
|
||||
logger.Field("login_type", req.LoginType),
|
||||
logger.Field("login_ip", ip),
|
||||
logger.Field("user_agent", r.UserAgent()),
|
||||
logger.Field("session_limit", l.svcCtx.SessionLimit()),
|
||||
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
|
||||
logger.Field("devices_count", len(userInfo.UserDevices)),
|
||||
)
|
||||
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
Limit: l.svcCtx.SessionLimit(),
|
||||
|
||||
@@ -38,6 +38,17 @@ func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLog
|
||||
}
|
||||
|
||||
func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.LoginResponse, err error) {
|
||||
// 打印请求参数 (隐藏密码)
|
||||
l.Infow("UserLogin 请求参数",
|
||||
logger.Field("email", req.Email),
|
||||
logger.Field("password_len", len(req.Password)),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("ip", req.IP),
|
||||
logger.Field("user_agent", req.UserAgent),
|
||||
logger.Field("login_type", req.LoginType),
|
||||
logger.Field("cf_token", req.CfToken),
|
||||
)
|
||||
|
||||
loginStatus := false
|
||||
var userInfo *user.User
|
||||
// Record login status
|
||||
@@ -130,10 +141,14 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
// Check TTL
|
||||
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
|
||||
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("session_ttl", ttl.Seconds()),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
@@ -182,6 +197,21 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
|
||||
// 登录成功 - 打印详细信息用于调试
|
||||
l.Infow("========== 用户登录成功 ==========",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("device_id", deviceId),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("reuse_session", reuseSession),
|
||||
logger.Field("login_type", req.LoginType),
|
||||
logger.Field("login_ip", req.IP),
|
||||
logger.Field("user_agent", req.UserAgent),
|
||||
logger.Field("session_limit", l.svcCtx.SessionLimit()),
|
||||
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
|
||||
logger.Field("devices_count", len(userInfo.UserDevices)),
|
||||
)
|
||||
|
||||
loginStatus = true
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
|
||||
@@ -32,7 +32,7 @@ func (l *GetDownloadLinkLogic) GetDownloadLink(req *types.GetDownloadLinkRequest
|
||||
host := l.svcCtx.Config.Site.Host
|
||||
if host == "" {
|
||||
// 保底域名
|
||||
host = "tapi.airoport.co"
|
||||
host = "api.airoport.co"
|
||||
}
|
||||
|
||||
// 2. 版本号 (后续可以从数据库或配置中读取)
|
||||
@@ -53,8 +53,13 @@ func (l *GetDownloadLinkLogic) GetDownloadLink(req *types.GetDownloadLinkRequest
|
||||
ext = ".bin"
|
||||
}
|
||||
|
||||
// 4. 构建文件名: 平台-版本号-ic_邀请码.扩展名
|
||||
filename := fmt.Sprintf("%s-%s-ic_%s%s", req.Platform, version, req.InviteCode, ext)
|
||||
// 4. 构建文件名: 平台-版本号[-ic_邀请码].扩展名
|
||||
var filename string
|
||||
if req.InviteCode != "" {
|
||||
filename = fmt.Sprintf("%s-%s-ic_%s%s", req.Platform, version, req.InviteCode, ext)
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s-%s%s", req.Platform, version, ext)
|
||||
}
|
||||
|
||||
// 5. 构建完整 URL (Nginx 会拦截此路径进行虚拟更名处理)
|
||||
url := fmt.Sprintf("https://%s/v1/common/client/download/file/%s", host, filename)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetDownloadLinkLogic_GetDownloadLink(t *testing.T) {
|
||||
svcCtx := &svc.ServiceContext{
|
||||
Config: config.Config{
|
||||
Site: config.SiteConfig{
|
||||
Host: "test.example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
l := NewGetDownloadLinkLogic(ctx, svcCtx)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *types.GetDownloadLinkRequest
|
||||
wantSubStr []string // strings that should be in the URL
|
||||
notSubStr []string // strings that should NOT be in the URL
|
||||
}{
|
||||
{
|
||||
name: "With Invite Code",
|
||||
req: &types.GetDownloadLinkRequest{
|
||||
Platform: "windows",
|
||||
InviteCode: "TESTCODE",
|
||||
},
|
||||
wantSubStr: []string{"-ic_TESTCODE.exe"},
|
||||
notSubStr: []string{},
|
||||
},
|
||||
{
|
||||
name: "Without Invite Code",
|
||||
req: &types.GetDownloadLinkRequest{
|
||||
Platform: "mac",
|
||||
InviteCode: "",
|
||||
},
|
||||
wantSubStr: []string{".dmg"},
|
||||
notSubStr: []string{"-ic", "ic_"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp, err := l.GetDownloadLink(tt.req)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
|
||||
for _, s := range tt.wantSubStr {
|
||||
assert.Contains(t, resp.Url, s)
|
||||
}
|
||||
for _, s := range tt.notSubStr {
|
||||
assert.NotContains(t, resp.Url, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,11 @@ func NewEPayNotifyLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *EPayNotif
|
||||
}
|
||||
|
||||
func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
||||
l.Logger.Info("[EPayNotify] 收到支付回调",
|
||||
logger.Field("orderNo", req.OutTradeNo),
|
||||
logger.Field("tradeNo", req.TradeNo),
|
||||
logger.Field("tradeStatus", req.TradeStatus),
|
||||
logger.Field("money", req.Money))
|
||||
|
||||
// Find payment config
|
||||
data, ok := l.ctx.Request.Context().Value(constant.CtxKeyPayment).(*payment.Payment)
|
||||
@@ -51,6 +56,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo)
|
||||
}
|
||||
|
||||
l.Logger.Info("[EPayNotify] 找到订单",
|
||||
logger.Field("orderNo", orderInfo.OrderNo),
|
||||
logger.Field("currentStatus", orderInfo.Status),
|
||||
logger.Field("userId", orderInfo.UserId),
|
||||
logger.Field("orderType", orderInfo.Type))
|
||||
|
||||
var config payment.EPayConfig
|
||||
if err := json.Unmarshal([]byte(data.Config), &config); err != nil {
|
||||
l.Logger.Errorw("[EPayNotify] Unmarshal config failed", logger.Field("error", err.Error()))
|
||||
@@ -59,7 +70,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
||||
// Verify sign
|
||||
client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type)
|
||||
if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug {
|
||||
l.Logger.Error("[EPayNotify] Verify sign failed")
|
||||
l.Logger.Error("[EPayNotify] Verify sign failed", logger.Field("orderNo", req.OutTradeNo))
|
||||
return nil
|
||||
}
|
||||
if req.TradeStatus != "TRADE_SUCCESS" {
|
||||
@@ -67,9 +78,11 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
||||
return nil
|
||||
}
|
||||
if orderInfo.Status == 5 {
|
||||
l.Logger.Info("[EPayNotify] 订单已完成,跳过处理", logger.Field("orderNo", req.OutTradeNo))
|
||||
return nil
|
||||
}
|
||||
// Update order status
|
||||
l.Logger.Info("[EPayNotify] 更新订单状态为已支付(2)", logger.Field("orderNo", req.OutTradeNo))
|
||||
err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OutTradeNo, 2)
|
||||
if err != nil {
|
||||
l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
|
||||
@@ -87,10 +100,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
|
||||
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
|
||||
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
|
||||
if err != nil {
|
||||
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()))
|
||||
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
|
||||
return err
|
||||
}
|
||||
l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo))
|
||||
l.Logger.Info("[EPayNotify] ✅ 回调处理成功,已入队激活任务",
|
||||
logger.Field("orderNo", req.OutTradeNo),
|
||||
logger.Field("taskId", taskInfo.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
|
||||
l.Errorw("清理原SessionId缓存失败", logger.Field("error", err.Error()), logger.Field("session_id", currentSessionId))
|
||||
// 不返回错误,继续执行
|
||||
} else {
|
||||
l.Infow("已清理原SessionId缓存", logger.Field("session_id", currentSessionId))
|
||||
l.Infow("[SessionMonitor] 绑定邮箱成功后立即清理原 Session", logger.Field("session_id", currentSessionId))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ func (l *BindInviteCodeLogic) BindInviteCode(req *types.BindInviteCodeRequest) e
|
||||
|
||||
// 检查用户是否已经绑定过邀请码
|
||||
if currentUser.RefererId != 0 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user already bound invite code")
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.UserBindInviteCodeExist), "用户已绑定邀请人")
|
||||
}
|
||||
|
||||
// 查找邀请人
|
||||
|
||||
@@ -139,6 +139,10 @@ func (l *DeleteAccountLogic) clearCurrentSession(userId int64) {
|
||||
// 从用户会话集合中移除当前session
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
|
||||
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err()
|
||||
|
||||
l.Infow("[SessionMonitor] 注销账号清除 Session",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("session_id", sessionId))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,8 +126,9 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
l.svcCtx.DeviceManager.KickDevice(u.Id, identifier)
|
||||
// clean user cache
|
||||
_ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u)
|
||||
l.Infow("设备解绑完成",
|
||||
l.Infow("[SessionMonitor] 设备解绑触发 Session 清理",
|
||||
logger.Field("device_identifier", identifier),
|
||||
logger.Field("user_id", u.Id),
|
||||
logger.Field("elapsed_ms", duration.Milliseconds()))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
jwtGo "github.com/golang-jwt/jwt/v5"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
@@ -36,7 +37,25 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
||||
// parse token
|
||||
claims, err := jwt.ParseJwtToken(token, jwtConfig.AccessSecret)
|
||||
if err != nil {
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] parse token failed", logger.Field("error", err.Error()))
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] parse token failed",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
|
||||
// [AuthDebug] Try to parse unverified to see why it failed (expired or invalid signature)
|
||||
parser := jwtGo.NewParser()
|
||||
unverifiedToken, _, _ := parser.ParseUnverified(token, jwtGo.MapClaims{})
|
||||
if unverifiedToken != nil {
|
||||
if unverifiedClaims, ok := unverifiedToken.Claims.(jwtGo.MapClaims); ok {
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthDebug] Token Parsing Failure Details",
|
||||
logger.Field("exp", unverifiedClaims["exp"]),
|
||||
logger.Field("iat", unverifiedClaims["iat"]),
|
||||
logger.Field("uid", unverifiedClaims["UserId"]),
|
||||
logger.Field("sid", unverifiedClaims["SessionId"]),
|
||||
logger.Field("sub", unverifiedClaims["sub"]),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.ErrorTokenExpire), "Token Invalid"))
|
||||
c.Abort()
|
||||
return
|
||||
@@ -60,9 +79,17 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
||||
value, err := svc.Redis.Get(c, sessionIdCacheKey).Result()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] session not found", logger.Field("sessionId", sessionId))
|
||||
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] Session无效或已过期",
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("redis_key", sessionIdCacheKey),
|
||||
logger.Field("ip", c.ClientIP()),
|
||||
logger.Field("path", c.Request.URL.Path))
|
||||
} else {
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] redis get failed", logger.Field("error", err.Error()), logger.Field("sessionId", sessionId))
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] Redis 查询失败",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("session_id", sessionId),
|
||||
logger.Field("redis_key", sessionIdCacheKey))
|
||||
}
|
||||
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access"))
|
||||
c.Abort()
|
||||
@@ -71,7 +98,10 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
||||
|
||||
//verify user id
|
||||
if value != fmt.Sprintf("%v", userId) {
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] user mismatch", logger.Field("userId", userId), logger.Field("sessionId", sessionId), logger.Field("value", value))
|
||||
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] user mismatch",
|
||||
logger.Field("userId_in_token", userId),
|
||||
logger.Field("userId_in_redis", value),
|
||||
logger.Field("sessionId", sessionId))
|
||||
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access"))
|
||||
c.Abort()
|
||||
return
|
||||
@@ -92,7 +122,17 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] auth ok", logger.Field("userId", userId), logger.Field("loginType", loginType), logger.Field("path", c.Request.URL.Path))
|
||||
// Get TTL details for debugging
|
||||
ttl, _ := svc.Redis.TTL(c.Request.Context(), sessionIdCacheKey).Result()
|
||||
|
||||
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] auth ok",
|
||||
logger.Field("userId", userId),
|
||||
logger.Field("loginType", loginType),
|
||||
logger.Field("path", c.Request.URL.Path),
|
||||
logger.Field("session_ttl", ttl.Seconds()),
|
||||
logger.Field("sessionId", sessionId),
|
||||
logger.Field("deviceId", deviceId),
|
||||
)
|
||||
ctx = context.WithValue(ctx, constant.LoginType, loginType)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
|
||||
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/redis"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/initialize"
|
||||
"github.com/perfect-panel/server/internal/handler"
|
||||
"github.com/perfect-panel/server/internal/middleware"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@@ -32,8 +31,6 @@ func NewService(svc *svc.ServiceContext) *Service {
|
||||
|
||||
func initServer(svc *svc.ServiceContext) *gin.Engine {
|
||||
|
||||
// start init system config
|
||||
initialize.StartInitSystemConfig(svc)
|
||||
// init gin server
|
||||
r := gin.Default()
|
||||
r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/perfect-panel/server/internal/model/auth"
|
||||
"github.com/perfect-panel/server/internal/model/coupon"
|
||||
"github.com/perfect-panel/server/internal/model/document"
|
||||
iapapple "github.com/perfect-panel/server/internal/model/iap/apple"
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/perfect-panel/server/internal/model/ticket"
|
||||
"github.com/perfect-panel/server/internal/model/traffic"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
iapapple "github.com/perfect-panel/server/internal/model/iap/apple"
|
||||
"github.com/perfect-panel/server/pkg/limit"
|
||||
"github.com/perfect-panel/server/pkg/nodeMultiplier"
|
||||
"github.com/perfect-panel/server/pkg/orm"
|
||||
@@ -56,13 +56,13 @@ type ServiceContext struct {
|
||||
ClientModel client.Model
|
||||
TicketModel ticket.Model
|
||||
//ServerModel server.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
SystemModel system.Model
|
||||
CouponModel coupon.Model
|
||||
PaymentModel payment.Model
|
||||
DocumentModel document.Model
|
||||
SubscribeModel subscribe.Model
|
||||
TrafficLogModel traffic.Model
|
||||
AnnouncementModel announcement.Model
|
||||
IAPAppleTransactionModel iapapple.Model
|
||||
|
||||
Restart func() error
|
||||
@@ -126,6 +126,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
|
||||
}
|
||||
|
||||
func (srv *ServiceContext) SessionLimit() int64 {
|
||||
// check custom data
|
||||
cd := srv.Config.Site.CustomData
|
||||
if cd != "" {
|
||||
var obj map[string]interface{}
|
||||
@@ -134,10 +135,12 @@ func (srv *ServiceContext) SessionLimit() int64 {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
if val > 0 {
|
||||
fmt.Printf("[SessionLimit] Using CustomData deviceLimit: %d\n", int64(val))
|
||||
return int64(val)
|
||||
}
|
||||
case string:
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 {
|
||||
fmt.Printf("[SessionLimit] Using CustomData deviceLimit: %d\n", n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
@@ -146,16 +149,19 @@ func (srv *ServiceContext) SessionLimit() int64 {
|
||||
switch val := v.(type) {
|
||||
case float64:
|
||||
if val > 0 {
|
||||
fmt.Printf("[SessionLimit] Using CustomData DeviceLimit: %d\n", int64(val))
|
||||
return int64(val)
|
||||
}
|
||||
case string:
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 {
|
||||
fmt.Printf("[SessionLimit] Using CustomData DeviceLimit: %d\n", n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Printf("[SessionLimit] Using Config MaxSessionsPerUser: %d\n", srv.Config.JwtAuth.MaxSessionsPerUser)
|
||||
return srv.Config.JwtAuth.MaxSessionsPerUser
|
||||
}
|
||||
|
||||
@@ -173,6 +179,22 @@ func (srv *ServiceContext) EnforceUserSessionLimit(ctx context.Context, userId i
|
||||
return err
|
||||
}
|
||||
if count > max {
|
||||
// [SessionDebug] Log all current sessions before eviction
|
||||
// Fetch all sessions (oldest to newest)
|
||||
sessions, _ := srv.Redis.ZRange(ctx, sessionsKey, 0, -1).Result()
|
||||
fmt.Printf("[SessionMonitor] ⚠️ Session Limit Exceeded (Count: %d, Max: %d). User %d has the following active sessions:\n", count, max, userId)
|
||||
|
||||
for i, sid := range sessions {
|
||||
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)
|
||||
val, err := srv.Redis.Get(ctx, detailKey).Result()
|
||||
if err == nil {
|
||||
fmt.Printf(" [%d] SessionID: %s | Detail: %s\n", i+1, sid, val)
|
||||
} else {
|
||||
fmt.Printf(" [%d] SessionID: %s | (No Detail - Likely old session)\n", i+1, sid)
|
||||
}
|
||||
}
|
||||
|
||||
// Log before eviction
|
||||
popped, err := srv.Redis.ZPopMin(ctx, sessionsKey, count-max).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -180,6 +202,12 @@ func (srv *ServiceContext) EnforceUserSessionLimit(ctx context.Context, userId i
|
||||
for _, z := range popped {
|
||||
sid := fmt.Sprintf("%v", z.Member)
|
||||
_ = srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sid)).Err()
|
||||
// Also delete detail
|
||||
_ = srv.Redis.Del(ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)).Err()
|
||||
|
||||
// 记录被踢出的 Session 信息
|
||||
fmt.Printf("[SessionMonitor] ❌ KICKED OUT Session: user_id=%d session_id=%s reason=exceed_limit\n",
|
||||
userId, sid)
|
||||
}
|
||||
}
|
||||
_ = srv.Redis.Expire(ctx, sessionsKey, time.Duration(srv.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
|
||||
Reference in New Issue
Block a user