This commit is contained in:
@@ -28,6 +28,7 @@ type Config struct {
|
||||
Register RegisterConfig `yaml:"Register"`
|
||||
Subscribe SubscribeConfig `yaml:"Subscribe"`
|
||||
Invite InviteConfig `yaml:"Invite"`
|
||||
Kutt KuttConfig `yaml:"Kutt"`
|
||||
Telegram Telegram `yaml:"Telegram"`
|
||||
Log Log `yaml:"Log"`
|
||||
Trace trace.Config `yaml:"Trace"`
|
||||
@@ -209,6 +210,15 @@ type InviteConfig struct {
|
||||
GiftDays int64 `yaml:"GiftDays" default:"0"`
|
||||
}
|
||||
|
||||
// KuttConfig Kutt 短链接服务配置
|
||||
type KuttConfig struct {
|
||||
Enable bool `yaml:"Enable" default:"false"` // 是否启用 Kutt 短链接
|
||||
ApiURL string `yaml:"ApiURL" default:""` // Kutt API 地址
|
||||
ApiKey string `yaml:"ApiKey" default:""` // Kutt API 密钥
|
||||
TargetURL string `yaml:"TargetURL" default:""` // 目标注册页面基础 URL
|
||||
Domain string `yaml:"Domain" default:""` // 短链接域名 (例如: getsapp.net)
|
||||
}
|
||||
|
||||
type Telegram struct {
|
||||
Enable bool `yaml:"Enable" default:"false"`
|
||||
BotID int64 `yaml:"BotID" default:""`
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/application"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
func CreateAppVersionHandler(svcCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req types.CreateAppVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svcCtx.Validate(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := application.NewCreateAppVersionLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.CreateAppVersion(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/application"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
func DeleteAppVersionHandler(svcCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req types.DeleteAppVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svcCtx.Validate(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := application.NewDeleteAppVersionLogic(c.Request.Context(), svcCtx)
|
||||
err := l.DeleteAppVersion(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/application"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
func GetAppVersionListHandler(svcCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetAppVersionListRequest
|
||||
if err := c.ShouldBindQuery(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Validation might be optional for GET if no required params
|
||||
if err := svcCtx.Validate(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := application.NewGetAppVersionListLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetAppVersionList(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/application"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
func UpdateAppVersionHandler(svcCtx *svc.ServiceContext) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req types.UpdateAppVersionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := svcCtx.Validate(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := application.NewUpdateAppVersionLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.UpdateAppVersion(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get subscribe application list
|
||||
adminApplicationGroupRouter.GET("/subscribe_application_list", adminApplication.GetSubscribeApplicationListHandler(serverCtx))
|
||||
|
||||
// Create App Version
|
||||
adminApplicationGroupRouter.POST("/version", adminApplication.CreateAppVersionHandler(serverCtx))
|
||||
|
||||
// Update App Version
|
||||
adminApplicationGroupRouter.PUT("/version", adminApplication.UpdateAppVersionHandler(serverCtx))
|
||||
|
||||
// Delete App Version
|
||||
adminApplicationGroupRouter.DELETE("/version", adminApplication.DeleteAppVersionHandler(serverCtx))
|
||||
|
||||
// Get App Version List
|
||||
adminApplicationGroupRouter.GET("/version/list", adminApplication.GetAppVersionListHandler(serverCtx))
|
||||
}
|
||||
|
||||
adminAuthMethodGroupRouter := router.Group("/v1/admin/auth-method")
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/client"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type CreateAppVersionLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCreateAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateAppVersionLogic {
|
||||
return &CreateAppVersionLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CreateAppVersionLogic) CreateAppVersion(req *types.CreateAppVersionRequest) (resp *types.ApplicationVersion, err error) {
|
||||
// Defaults
|
||||
isDefault := false
|
||||
if req.IsDefault != nil {
|
||||
isDefault = *req.IsDefault
|
||||
}
|
||||
isInReview := false
|
||||
if req.IsInReview != nil {
|
||||
isInReview = *req.IsInReview
|
||||
}
|
||||
|
||||
description := json.RawMessage(req.Description)
|
||||
|
||||
version := &client.ApplicationVersion{
|
||||
Platform: req.Platform,
|
||||
Version: req.Version,
|
||||
MinVersion: req.MinVersion,
|
||||
ForceUpdate: req.ForceUpdate,
|
||||
Url: req.Url,
|
||||
Description: description,
|
||||
IsDefault: isDefault,
|
||||
IsInReview: isInReview,
|
||||
}
|
||||
|
||||
if err := l.svcCtx.DB.Create(version).Error; err != nil {
|
||||
l.Errorw("[CreateAppVersion] create version error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create version error: %v", err)
|
||||
}
|
||||
|
||||
// Manual mapping to types.ApplicationVersion
|
||||
resp = &types.ApplicationVersion{
|
||||
Id: version.Id,
|
||||
Platform: version.Platform, // Note: types.ApplicationVersion might not have Platform field based on previous view_file. Let's check.
|
||||
Version: version.Version,
|
||||
MinVersion: version.MinVersion,
|
||||
ForceUpdate: version.ForceUpdate,
|
||||
Description: make(map[string]string), // Simplified for now
|
||||
Url: version.Url,
|
||||
IsDefault: version.IsDefault,
|
||||
IsInReview: version.IsInReview,
|
||||
CreatedAt: version.CreatedAt.Unix(),
|
||||
}
|
||||
// Try to unmarshal description
|
||||
_ = json.Unmarshal(version.Description, &resp.Description)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/client"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type DeleteAppVersionLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewDeleteAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAppVersionLogic {
|
||||
return &DeleteAppVersionLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DeleteAppVersionLogic) DeleteAppVersion(req *types.DeleteAppVersionRequest) error {
|
||||
if err := l.svcCtx.DB.Delete(&client.ApplicationVersion{}, req.Id).Error; err != nil {
|
||||
l.Errorw("[DeleteAppVersion] delete version error", logger.Field("error", err.Error()))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete version error: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/client"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetAppVersionListLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewGetAppVersionListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAppVersionListLogic {
|
||||
return &GetAppVersionListLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetAppVersionListLogic) GetAppVersionList(req *types.GetAppVersionListRequest) (resp *types.GetAppVersionListResponse, err error) {
|
||||
var versions []*client.ApplicationVersion
|
||||
var total int64
|
||||
|
||||
db := l.svcCtx.DB.Model(&client.ApplicationVersion{})
|
||||
|
||||
if req.Platform != "" {
|
||||
db = db.Where("platform = ?", req.Platform)
|
||||
}
|
||||
|
||||
err = db.Count(&total).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get version list count error: %v", err)
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.Size
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
err = db.Offset(offset).Limit(req.Size).Order("id desc").Find(&versions).Error
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get version list error: %v", err)
|
||||
}
|
||||
|
||||
var list []*types.ApplicationVersion
|
||||
for _, v := range versions {
|
||||
desc := make(map[string]string)
|
||||
_ = json.Unmarshal(v.Description, &desc)
|
||||
list = append(list, &types.ApplicationVersion{
|
||||
Id: v.Id,
|
||||
Platform: v.Platform,
|
||||
Version: v.Version,
|
||||
MinVersion: v.MinVersion,
|
||||
ForceUpdate: v.ForceUpdate,
|
||||
Description: desc,
|
||||
Url: v.Url,
|
||||
IsDefault: v.IsDefault,
|
||||
IsInReview: v.IsInReview,
|
||||
CreatedAt: v.CreatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
return &types.GetAppVersionListResponse{
|
||||
Total: total,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/client"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UpdateAppVersionLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewUpdateAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateAppVersionLogic {
|
||||
return &UpdateAppVersionLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UpdateAppVersionLogic) UpdateAppVersion(req *types.UpdateAppVersionRequest) (resp *types.ApplicationVersion, err error) {
|
||||
// Defaults
|
||||
isDefault := false
|
||||
if req.IsDefault != nil {
|
||||
isDefault = *req.IsDefault
|
||||
}
|
||||
isInReview := false
|
||||
if req.IsInReview != nil {
|
||||
isInReview = *req.IsInReview
|
||||
}
|
||||
|
||||
description := json.RawMessage(req.Description)
|
||||
|
||||
version := &client.ApplicationVersion{
|
||||
Id: req.Id,
|
||||
Platform: req.Platform,
|
||||
Version: req.Version,
|
||||
MinVersion: req.MinVersion,
|
||||
ForceUpdate: req.ForceUpdate,
|
||||
Url: req.Url,
|
||||
Description: description,
|
||||
IsDefault: isDefault,
|
||||
IsInReview: isInReview,
|
||||
}
|
||||
|
||||
if err := l.svcCtx.DB.Save(version).Error; err != nil {
|
||||
l.Errorw("[UpdateAppVersion] update version error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update version error: %v", err)
|
||||
}
|
||||
|
||||
resp = &types.ApplicationVersion{
|
||||
Id: version.Id,
|
||||
Platform: version.Platform,
|
||||
Version: version.Version,
|
||||
MinVersion: version.MinVersion,
|
||||
ForceUpdate: version.ForceUpdate,
|
||||
Description: make(map[string]string),
|
||||
Url: version.Url,
|
||||
IsDefault: version.IsDefault,
|
||||
IsInReview: version.IsInReview,
|
||||
CreatedAt: version.CreatedAt.Unix(),
|
||||
}
|
||||
_ = json.Unmarshal(version.Description, &resp.Description)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -120,10 +120,42 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
}
|
||||
}
|
||||
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// 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)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && 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 != "" {
|
||||
// Verify session belongs to current user (防止设备转移后复用其他用户的session)
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
logger.Field("current_user_id", userInfo.Id),
|
||||
logger.Field("session_user_id", uidStr),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reuseSession {
|
||||
sessionId = uuidx.NewUUID().String()
|
||||
l.Infow("creating new session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
)
|
||||
}
|
||||
|
||||
// Generate token
|
||||
// Generate token (always generate new token, but may reuse sessionId)
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
@@ -141,23 +173,14 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||
}
|
||||
|
||||
// If device had a previous session, invalidate it first (MUST be before EnforceUserSessionLimit)
|
||||
oldDeviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, oldDeviceCacheKey).Result(); getErr == nil && oldSid != "" {
|
||||
oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid)
|
||||
if uidStr, _ := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); uidStr != "" {
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, oldSessionKey).Err()
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, uidStr)
|
||||
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, oldSid).Err()
|
||||
// Only enforce session limit and add to user sessions if this is a new session
|
||||
if !reuseSession {
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, oldDeviceCacheKey).Err()
|
||||
}
|
||||
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Store session id in redis
|
||||
// Store/refresh session id in redis (extend TTL)
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
l.Errorw("set session id error",
|
||||
@@ -167,8 +190,7 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Store device id in redis
|
||||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
// Store/refresh device-to-session mapping (extend TTL)
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
l.Errorw("set device id error",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
|
||||
@@ -220,7 +220,40 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Check if device has an existing valid session - reuse it instead of creating new one
|
||||
var sessionId string
|
||||
var reuseSession bool
|
||||
var deviceCacheKey string
|
||||
if req.Identifier != "" {
|
||||
deviceCacheKey = fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && 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 != "" {
|
||||
// Verify session belongs to current user (防止设备转移后复用其他用户的session)
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
logger.Field("current_user_id", userInfo.Id),
|
||||
logger.Field("session_user_id", uidStr),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reuseSession {
|
||||
sessionId = uuidx.NewUUID().String()
|
||||
}
|
||||
|
||||
// Generate token (always generate new token, but may reuse sessionId)
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
@@ -233,32 +266,22 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||
}
|
||||
// If device had a previous session, invalidate it first (MUST be before EnforceUserSessionLimit)
|
||||
if req.Identifier != "" {
|
||||
oldDeviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, oldDeviceCacheKey).Result(); getErr == nil && oldSid != "" {
|
||||
oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid)
|
||||
if uidStr, _ := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); uidStr != "" {
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, oldSessionKey).Err()
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, uidStr)
|
||||
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, oldSid).Err()
|
||||
}
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, oldDeviceCacheKey).Err()
|
||||
|
||||
// Only enforce session limit and add to user sessions if this is a new session
|
||||
if !reuseSession {
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
// Store/refresh session id in redis (extend TTL)
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Store device-to-session mapping
|
||||
// Store/refresh device-to-session mapping (extend TTL)
|
||||
if req.Identifier != "" {
|
||||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
|
||||
|
||||
@@ -144,9 +144,40 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
// Check if device has an existing valid session - reuse it instead of creating new one
|
||||
var sessionId string
|
||||
var reuseSession bool
|
||||
var deviceCacheKey string
|
||||
if req.Identifier != "" {
|
||||
deviceCacheKey = fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && 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 != "" {
|
||||
// Verify session belongs to current user (防止设备转移后复用其他用户的session)
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
logger.Field("current_user_id", userInfo.Id),
|
||||
logger.Field("session_user_id", uidStr),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reuseSession {
|
||||
sessionId = uuidx.NewUUID().String()
|
||||
}
|
||||
|
||||
// Generate token (always generate new token, but may reuse sessionId)
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
@@ -159,13 +190,24 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||
}
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
|
||||
// Only enforce session limit and add to user sessions if this is a new session
|
||||
if !reuseSession {
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Store/refresh session id in redis (extend TTL)
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Store/refresh device-to-session mapping (extend TTL)
|
||||
if req.Identifier != "" {
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
loginStatus = true
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
|
||||
@@ -115,9 +115,41 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
req.LoginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
// Generate session id
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
// Generate token
|
||||
|
||||
// Check if device has an existing valid session - reuse it instead of creating new one
|
||||
var sessionId string
|
||||
var reuseSession bool
|
||||
var deviceCacheKey string
|
||||
if req.Identifier != "" {
|
||||
deviceCacheKey = fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && 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 != "" {
|
||||
// Verify session belongs to current user (防止设备转移后复用其他用户的session)
|
||||
if uidStr == fmt.Sprintf("%d", userInfo.Id) {
|
||||
sessionId = oldSid
|
||||
reuseSession = true
|
||||
l.Infow("reusing existing session for device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
logger.Field("session_id", sessionId),
|
||||
)
|
||||
} else {
|
||||
l.Infow("device session belongs to different user, creating new session",
|
||||
logger.Field("current_user_id", userInfo.Id),
|
||||
logger.Field("session_user_id", uidStr),
|
||||
logger.Field("identifier", req.Identifier),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !reuseSession {
|
||||
sessionId = uuidx.NewUUID().String()
|
||||
}
|
||||
|
||||
// Generate token (always generate new token, but may reuse sessionId)
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
@@ -131,32 +163,22 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
|
||||
l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||
}
|
||||
// If device had a previous session, invalidate it first (MUST be before EnforceUserSessionLimit)
|
||||
if req.Identifier != "" {
|
||||
oldDeviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, oldDeviceCacheKey).Result(); getErr == nil && oldSid != "" {
|
||||
oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid)
|
||||
if uidStr, _ := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); uidStr != "" {
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, oldSessionKey).Err()
|
||||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, uidStr)
|
||||
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, oldSid).Err()
|
||||
}
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, oldDeviceCacheKey).Err()
|
||||
|
||||
// Only enforce session limit and add to user sessions if this is a new session
|
||||
if !reuseSession {
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if err = l.svcCtx.EnforceUserSessionLimit(l.ctx, userInfo.Id, sessionId, l.svcCtx.SessionLimit()); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "enforce session limit error: %v", err.Error())
|
||||
}
|
||||
// Store/refresh session id in redis (extend TTL)
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, userInfo.Id, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||
}
|
||||
|
||||
// Store device-to-session mapping
|
||||
// Store/refresh device-to-session mapping (extend TTL)
|
||||
if req.Identifier != "" {
|
||||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
|
||||
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/client"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
@@ -25,16 +27,37 @@ func NewGetAppVersionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Get
|
||||
|
||||
// GetAppVersion 根据平台返回最新版本信息
|
||||
func (l *GetAppVersionLogic) GetAppVersion(req *types.GetAppVersionRequest) (resp *types.ApplicationVersion, err error) {
|
||||
// TODO: 后续对接数据库实现
|
||||
resp = &types.ApplicationVersion{
|
||||
Version: "1.0.0",
|
||||
MinVersion: "1.0.0",
|
||||
ForceUpdate: false,
|
||||
Description: map[string]string{
|
||||
"zh-CN": "初始版本",
|
||||
"en-US": "Initial version",
|
||||
},
|
||||
IsDefault: true,
|
||||
// Query the latest version for the platform
|
||||
var version client.ApplicationVersion
|
||||
err = l.svcCtx.DB.Model(&client.ApplicationVersion{}).
|
||||
Where("platform = ? AND is_default = 1", req.Platform).
|
||||
Order("id desc").First(&version).Error
|
||||
|
||||
if err != nil {
|
||||
l.Errorf("[GetAppVersion] get version error: %v", err)
|
||||
// Return empty or default if not found
|
||||
return &types.ApplicationVersion{
|
||||
Version: "unknown",
|
||||
MinVersion: "unknown",
|
||||
ForceUpdate: false,
|
||||
Description: map[string]string{},
|
||||
IsDefault: false,
|
||||
}, nil
|
||||
}
|
||||
return
|
||||
|
||||
resp = &types.ApplicationVersion{
|
||||
Id: version.Id,
|
||||
Platform: version.Platform,
|
||||
Version: version.Version,
|
||||
MinVersion: version.MinVersion,
|
||||
ForceUpdate: version.ForceUpdate,
|
||||
Description: make(map[string]string),
|
||||
Url: version.Url,
|
||||
IsDefault: version.IsDefault,
|
||||
IsInReview: version.IsInReview,
|
||||
CreatedAt: version.CreatedAt.Unix(),
|
||||
}
|
||||
_ = json.Unmarshal(version.Description, &resp.Description)
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -296,6 +296,15 @@ func (l *BindEmailWithVerificationLogic) addAuthMethodForEmailUser(userId int64,
|
||||
l.Infow("成功添加邮箱用户认证方法",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("email", email))
|
||||
|
||||
// 清理用户缓存,确保下次查询能获取到新的认证列表
|
||||
if user, err := l.svcCtx.UserModel.FindOne(l.ctx, userId); err == nil && user != nil {
|
||||
if err := l.svcCtx.UserModel.BatchClearRelatedCache(l.ctx, user); err != nil {
|
||||
l.Errorw("清理用户缓存失败", logger.Field("error", err.Error()), logger.Field("user_id", userId))
|
||||
} else {
|
||||
l.Infow("清理用户缓存成功", logger.Field("user_id", userId))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,12 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/kutt"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -40,15 +43,29 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
|
||||
}
|
||||
tool.DeepCopy(resp, u)
|
||||
|
||||
// 临时调试日志:打印原始 AuthMethods
|
||||
fmt.Println("========================================")
|
||||
fmt.Printf("UserID: %d, Original AuthMethods Count: %d\n", u.Id, len(u.AuthMethods))
|
||||
for i, m := range u.AuthMethods {
|
||||
fmt.Printf(" [%d] Type: %s, Identifier: %s\n", i, m.AuthType, m.AuthIdentifier)
|
||||
}
|
||||
fmt.Println("========================================")
|
||||
|
||||
var userMethods []types.UserAuthMethod
|
||||
for _, method := range resp.AuthMethods {
|
||||
var item types.UserAuthMethod
|
||||
tool.DeepCopy(&item, method)
|
||||
for _, method := range u.AuthMethods {
|
||||
item := types.UserAuthMethod{
|
||||
AuthType: method.AuthType,
|
||||
Verified: method.Verified,
|
||||
AuthIdentifier: method.AuthIdentifier,
|
||||
}
|
||||
|
||||
switch method.AuthType {
|
||||
case "mobile":
|
||||
item.AuthIdentifier = phone.MaskPhoneNumber(method.AuthIdentifier)
|
||||
case "email":
|
||||
// No masking for email
|
||||
case "device":
|
||||
// No masking for device identifier
|
||||
default:
|
||||
item.AuthIdentifier = maskOpenID(method.AuthIdentifier)
|
||||
}
|
||||
@@ -60,10 +77,133 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
|
||||
return getAuthTypePriority(userMethods[i].AuthType) < getAuthTypePriority(userMethods[j].AuthType)
|
||||
})
|
||||
|
||||
// 临时调试日志:打印处理后的 AuthMethods
|
||||
fmt.Println("========================================")
|
||||
fmt.Printf("UserID: %d, Sorted Response AuthMethods Count: %d\n", u.Id, len(userMethods))
|
||||
for i, m := range userMethods {
|
||||
fmt.Printf(" [%d] Type: %s, Identifier: %s\n", i, m.AuthType, m.AuthIdentifier)
|
||||
}
|
||||
fmt.Println("========================================")
|
||||
|
||||
resp.AuthMethods = userMethods
|
||||
|
||||
// 生成邀请短链接
|
||||
if l.svcCtx.Config.Kutt.Enable && resp.ReferCode != "" {
|
||||
shortLink := l.generateInviteShortLink(resp.ReferCode)
|
||||
if shortLink != "" {
|
||||
resp.ShareLink = shortLink
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// customData 用于解析 SiteConfig.CustomData JSON 字段
|
||||
// 包含从自定义数据中提取所需的配置项
|
||||
type customData struct {
|
||||
ShareUrl string `json:"shareUrl"` // 分享链接前缀 URL(目标落地页)
|
||||
Domain string `json:"domain"` // 短链接域名
|
||||
}
|
||||
|
||||
// getShareUrl 从 SiteConfig.CustomData 中获取 shareUrl
|
||||
//
|
||||
// 返回:
|
||||
// - string: 分享链接前缀 URL,如果获取失败则返回 Kutt.TargetURL 作为 fallback
|
||||
func (l *QueryUserInfoLogic) getShareUrl() string {
|
||||
siteConfig := l.svcCtx.Config.Site
|
||||
if siteConfig.CustomData != "" {
|
||||
var data customData
|
||||
if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil {
|
||||
if data.ShareUrl != "" {
|
||||
return data.ShareUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback 到 Kutt.TargetURL
|
||||
return l.svcCtx.Config.Kutt.TargetURL
|
||||
}
|
||||
|
||||
// getDomain 从 SiteConfig.CustomData 中获取短链接域名
|
||||
//
|
||||
// 返回:
|
||||
// - string: 短链接域名,如果获取失败则返回 Kutt.Domain 作为 fallback
|
||||
func (l *QueryUserInfoLogic) getDomain() string {
|
||||
siteConfig := l.svcCtx.Config.Site
|
||||
if siteConfig.CustomData != "" {
|
||||
var data customData
|
||||
if err := json.Unmarshal([]byte(siteConfig.CustomData), &data); err == nil {
|
||||
if data.Domain != "" {
|
||||
return data.Domain
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback 到 Kutt.Domain
|
||||
return l.svcCtx.Config.Kutt.Domain
|
||||
}
|
||||
|
||||
// generateInviteShortLink 生成邀请短链接(带 Redis 缓存)
|
||||
//
|
||||
// 参数:
|
||||
// - inviteCode: 邀请码
|
||||
//
|
||||
// 返回:
|
||||
// - string: 短链接 URL,失败时返回空字符串
|
||||
func (l *QueryUserInfoLogic) generateInviteShortLink(inviteCode string) string {
|
||||
cfg := l.svcCtx.Config.Kutt
|
||||
shareUrl := l.getShareUrl()
|
||||
domain := l.getDomain()
|
||||
|
||||
// 检查必要配置
|
||||
if cfg.ApiURL == "" || cfg.ApiKey == "" {
|
||||
l.Sloww("Kutt config incomplete",
|
||||
logger.Field("api_url", cfg.ApiURL != ""),
|
||||
logger.Field("api_key", cfg.ApiKey != ""))
|
||||
return ""
|
||||
}
|
||||
if shareUrl == "" {
|
||||
l.Sloww("ShareUrl not configured in CustomData or Kutt.TargetURL")
|
||||
return ""
|
||||
}
|
||||
|
||||
// Redis 缓存 key
|
||||
cacheKey := "cache:invite:short_link:" + inviteCode
|
||||
|
||||
// 1. 尝试从 Redis 缓存读取
|
||||
cachedLink, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||
if err == nil && cachedLink != "" {
|
||||
l.Debugw("Hit cache for invite short link",
|
||||
logger.Field("invite_code", inviteCode),
|
||||
logger.Field("short_link", cachedLink))
|
||||
return cachedLink
|
||||
}
|
||||
|
||||
// 2. 缓存未命中,调用 Kutt API 创建短链接
|
||||
client := kutt.NewClient(cfg.ApiURL, cfg.ApiKey)
|
||||
shortLink, err := client.CreateInviteShortLink(l.ctx, shareUrl, inviteCode, domain)
|
||||
if err != nil {
|
||||
l.Errorw("Failed to create short link",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("invite_code", inviteCode),
|
||||
logger.Field("share_url", shareUrl))
|
||||
return ""
|
||||
}
|
||||
|
||||
// 3. 写入 Redis 缓存(永不过期,因为邀请码不变短链接也不会变)
|
||||
if err := l.svcCtx.Redis.Set(l.ctx, cacheKey, shortLink, 0).Err(); err != nil {
|
||||
l.Errorw("Failed to cache short link",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("invite_code", inviteCode))
|
||||
// 缓存失败不影响返回
|
||||
}
|
||||
|
||||
l.Infow("Created and cached invite short link",
|
||||
logger.Field("invite_code", inviteCode),
|
||||
logger.Field("short_link", shortLink),
|
||||
logger.Field("share_url", shareUrl))
|
||||
|
||||
return shortLink
|
||||
}
|
||||
|
||||
// getAuthTypePriority 获取认证类型的排序优先级
|
||||
// email: 1 (第一位)
|
||||
// mobile: 2 (第二位)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApplicationVersion struct {
|
||||
Id int64 `gorm:"primaryKey"`
|
||||
Platform string `gorm:"type:varchar(50);not null;comment:Platform (ios, android, windows, mac, linux, harmony)"`
|
||||
Version string `gorm:"type:varchar(50);not null;comment:Version Number"`
|
||||
MinVersion string `gorm:"type:varchar(50);default:null;comment:Minimum Force Update Version"`
|
||||
ForceUpdate bool `gorm:"type:tinyint(1);not null;default:0;comment:Force Update"`
|
||||
Url string `gorm:"type:varchar(255);not null;comment:Download URL"`
|
||||
Description json.RawMessage `gorm:"type:json;default:null;comment:Update Description (JSON for multi-language)"`
|
||||
IsDefault bool `gorm:"type:tinyint(1);not null;default:0;comment:Is Default Version"`
|
||||
IsInReview bool `gorm:"type:tinyint(1);not null;default:0;comment:Is In Review"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
func (ApplicationVersion) TableName() string {
|
||||
return "application_versions"
|
||||
}
|
||||
+44
-3
@@ -109,6 +109,7 @@ type ApplicationResponseInfo struct {
|
||||
|
||||
type ApplicationVersion struct {
|
||||
Id int64 `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
Url string `json:"url"`
|
||||
Version string `json:"version" validate:"required"`
|
||||
MinVersion string `json:"min_version"`
|
||||
@@ -487,9 +488,10 @@ type Currency struct {
|
||||
}
|
||||
|
||||
type CurrencyConfig struct {
|
||||
AccessKey string `json:"access_key"`
|
||||
CurrencyUnit string `json:"currency_unit"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
AccessKey string `json:"access_key"`
|
||||
CurrencyUnit string `json:"currency_unit"`
|
||||
CurrencySymbol string `json:"currency_symbol"`
|
||||
FixedRate float64 `json:"fixed_rate"`
|
||||
}
|
||||
|
||||
type DeleteAdsRequest struct {
|
||||
@@ -2614,6 +2616,7 @@ type User struct {
|
||||
GiftAmount int64 `json:"gift_amount"`
|
||||
Telegram int64 `json:"telegram"`
|
||||
ReferCode string `json:"refer_code"`
|
||||
ShareLink string `json:"share_link,omitempty"`
|
||||
RefererId int64 `json:"referer_id"`
|
||||
Enable bool `json:"enable"`
|
||||
IsAdmin bool `json:"is_admin,omitempty"`
|
||||
@@ -2924,3 +2927,41 @@ type GetAppleStatusResponse struct {
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Tier string `json:"tier"`
|
||||
}
|
||||
|
||||
type CreateAppVersionRequest struct {
|
||||
Platform string `json:"platform" validate:"required,oneof=ios macos linux android windows harmony"`
|
||||
Version string `json:"version" validate:"required"`
|
||||
MinVersion string `json:"min_version"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
Description string `json:"description"`
|
||||
Url string `json:"url" validate:"required,url"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
IsInReview *bool `json:"is_in_review"`
|
||||
}
|
||||
|
||||
type UpdateAppVersionRequest struct {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
Platform string `json:"platform" validate:"required,oneof=ios macos linux android windows harmony"`
|
||||
Version string `json:"version" validate:"required"`
|
||||
MinVersion string `json:"min_version"`
|
||||
ForceUpdate bool `json:"force_update"`
|
||||
Description string `json:"description"`
|
||||
Url string `json:"url" validate:"required,url"`
|
||||
IsDefault *bool `json:"is_default"`
|
||||
IsInReview *bool `json:"is_in_review"`
|
||||
}
|
||||
|
||||
type DeleteAppVersionRequest struct {
|
||||
Id int64 `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type GetAppVersionListRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Platform string `form:"platform,optional"`
|
||||
}
|
||||
|
||||
type GetAppVersionListResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
List []*ApplicationVersion `json:"list"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user