各种配置项修复,优化到后台管理端配置
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
This commit is contained in:
parent
149dfe1ac3
commit
4752f844ef
@ -23,6 +23,10 @@ type (
|
||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||
ShortCode string `form:"short_code,omitempty"`
|
||||
FamilyJoined *bool `form:"family_joined,omitempty"`
|
||||
FamilyStatus string `form:"family_status,omitempty"`
|
||||
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
}
|
||||
// GetUserListResponse
|
||||
GetUserListResponse {
|
||||
@ -181,7 +185,7 @@ type (
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
DeleteUserSubscribeRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id,string"`
|
||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required,gt=0"`
|
||||
}
|
||||
GetUserSubscribeByIdRequest {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
@ -192,6 +196,35 @@ type (
|
||||
ResetUserSubscribeTrafficRequest {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
}
|
||||
GetFamilyListRequest {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Keyword string `form:"keyword,omitempty"`
|
||||
Status string `form:"status,omitempty"`
|
||||
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
}
|
||||
GetFamilyListResponse {
|
||||
List []FamilySummary `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
GetFamilyDetailRequest {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
}
|
||||
UpdateFamilyMaxMembersRequest {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
|
||||
}
|
||||
RemoveFamilyMemberRequest {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
UserId int64 `json:"user_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
DissolveFamilyRequest {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
@server (
|
||||
@ -312,4 +345,24 @@ service ppanel {
|
||||
@doc "Reset user subscribe traffic"
|
||||
@handler ResetUserSubscribeTraffic
|
||||
post /subscribe/reset/traffic (ResetUserSubscribeTrafficRequest)
|
||||
|
||||
@doc "Get family list"
|
||||
@handler GetFamilyList
|
||||
get /family/list (GetFamilyListRequest) returns (GetFamilyListResponse)
|
||||
|
||||
@doc "Get family detail"
|
||||
@handler GetFamilyDetail
|
||||
get /family/detail (GetFamilyDetailRequest) returns (FamilyDetail)
|
||||
|
||||
@doc "Update family max members"
|
||||
@handler UpdateFamilyMaxMembers
|
||||
put /family/max_members (UpdateFamilyMaxMembersRequest)
|
||||
|
||||
@doc "Remove family member"
|
||||
@handler RemoveFamilyMember
|
||||
put /family/member/remove (RemoveFamilyMemberRequest)
|
||||
|
||||
@doc "Dissolve family"
|
||||
@handler DissolveFamily
|
||||
put /family/dissolve (DissolveFamilyRequest)
|
||||
}
|
||||
|
||||
@ -33,6 +33,16 @@ type (
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||
IsDel bool `json:"is_del,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
PurchasedPackage string `json:"purchased_package,omitempty"`
|
||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
||||
FamilyId int64 `json:"family_id,omitempty"`
|
||||
FamilyRole uint8 `json:"family_role,omitempty"`
|
||||
FamilyRoleName string `json:"family_role_name,omitempty"`
|
||||
FamilyOwnerUserId int64 `json:"family_owner_user_id,omitempty"`
|
||||
FamilyStatus string `json:"family_status,omitempty"`
|
||||
FamilyMemberCount int64 `json:"family_member_count,omitempty"`
|
||||
FamilyMaxMembers int64 `json:"family_max_members,omitempty"`
|
||||
}
|
||||
Follow {
|
||||
Id int64 `json:"id"`
|
||||
@ -187,6 +197,7 @@ type (
|
||||
ForcedInvite bool `json:"forced_invite"`
|
||||
ReferralPercentage int64 `json:"referral_percentage"`
|
||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
||||
GiftDays int64 `json:"gift_days"`
|
||||
}
|
||||
TelegramConfig {
|
||||
TelegramBotToken string `json:"telegram_bot_token"`
|
||||
@ -530,6 +541,31 @@ type (
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
FamilySummary {
|
||||
FamilyId int64 `json:"family_id"`
|
||||
OwnerUserId int64 `json:"owner_user_id"`
|
||||
OwnerIdentifier string `json:"owner_identifier"`
|
||||
Status string `json:"status"`
|
||||
ActiveMemberCount int64 `json:"active_member_count"`
|
||||
MaxMembers int64 `json:"max_members"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
FamilyMemberItem {
|
||||
UserId int64 `json:"user_id"`
|
||||
Identifier string `json:"identifier"`
|
||||
Role uint8 `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Status uint8 `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
JoinSource string `json:"join_source"`
|
||||
JoinedAt int64 `json:"joined_at"`
|
||||
LeftAt int64 `json:"left_at,omitempty"`
|
||||
}
|
||||
FamilyDetail {
|
||||
Summary FamilySummary `json:"summary"`
|
||||
Members []FamilyMemberItem `json:"members"`
|
||||
}
|
||||
UserAuthMethod {
|
||||
AuthType string `json:"auth_type"`
|
||||
AuthIdentifier string `json:"auth_identifier"`
|
||||
@ -876,4 +912,3 @@ type (
|
||||
UserSubscribeId int64 `json:"user_subscribe_id"`
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
58
doc/cache-convention-zh.md
Normal file
58
doc/cache-convention-zh.md
Normal file
@ -0,0 +1,58 @@
|
||||
# GORM + Redis 缓存调用规范
|
||||
|
||||
本文档约定了项目内 GORM 与 Redis 缓存的推荐调用方式,目标是避免“有的地方自动清缓存,有的地方手动清缓存”的分散写法。
|
||||
|
||||
## 统一入口
|
||||
|
||||
项目缓存统一由 `pkg/cache/CachedConn` 提供,常用方法:
|
||||
|
||||
- `QueryCtx`:读缓存,未命中回源 DB 并写缓存
|
||||
- `ExecCtx`:写 DB,成功后删除指定缓存 key
|
||||
- `QueryNoCacheCtx`:只查 DB,不读写缓存
|
||||
- `ExecNoCacheCtx`:只写 DB,不自动清缓存
|
||||
- `TransactCtx`:事务执行
|
||||
|
||||
## 推荐使用规则
|
||||
|
||||
### 1) 主键/唯一键查询(有稳定 key)
|
||||
|
||||
优先使用 `QueryCtx`,必须显式构建缓存 key。
|
||||
|
||||
### 2) 列表/复杂筛选查询
|
||||
|
||||
仅当 key 规则稳定时用 `QueryCtx`;否则用 `QueryNoCacheCtx`。
|
||||
|
||||
### 3) 写操作(新增/更新/删除)
|
||||
|
||||
优先使用“统一 helper”封装为一条路径:
|
||||
|
||||
1. 执行 DB 写入
|
||||
2. 按模型计算 key
|
||||
3. 清理关联缓存
|
||||
|
||||
避免在业务逻辑层分散调用 `DelCache`。
|
||||
|
||||
## user_subscribe 规范落地
|
||||
|
||||
`internal/model/user/subscribe.go` 已提供统一 helper:
|
||||
|
||||
- `execSubscribeMutation(...)`
|
||||
|
||||
`InsertSubscribe / UpdateSubscribe / DeleteSubscribe / DeleteSubscribeById` 统一通过该 helper 执行,避免每个方法重复写“ExecNoCacheCtx + defer 清缓存”。
|
||||
|
||||
## key 生成约定
|
||||
|
||||
模型缓存 key 统一在模型侧定义,不在 handler/logic 手写:
|
||||
|
||||
- `internal/model/user/cache.go`
|
||||
- `(*Subscribe).GetCacheKeys()`
|
||||
- `ClearSubscribeCacheByModels(...)`
|
||||
|
||||
> 说明:`user_subscribe` 当前会同时清理普通列表 key 与 `:all` 列表 key,避免删改后列表残留旧缓存。
|
||||
|
||||
## 新增模型时的最小模板
|
||||
|
||||
1. 在 model 中定义 `getCacheKeys(...)` 或 `GetCacheKeys()`
|
||||
2. 查询方法优先 `QueryCtx`
|
||||
3. 写方法统一走 helper(DB 写 + 缓存失效)
|
||||
4. 避免在 handler/logic 直接操作缓存 key
|
||||
@ -12,7 +12,10 @@ import (
|
||||
func DeleteUserSubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.DeleteUserSubscribeRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
result.ParamErrorResult(c, err)
|
||||
return
|
||||
}
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
|
||||
26
internal/handler/admin/user/dissolveFamilyHandler.go
Normal file
26
internal/handler/admin/user/dissolveFamilyHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Dissolve family
|
||||
func DissolveFamilyHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.DissolveFamilyRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewDissolveFamilyLogic(c.Request.Context(), svcCtx)
|
||||
err := l.DissolveFamily(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
26
internal/handler/admin/user/getFamilyDetailHandler.go
Normal file
26
internal/handler/admin/user/getFamilyDetailHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get family detail
|
||||
func GetFamilyDetailHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetFamilyDetailRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewGetFamilyDetailLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetFamilyDetail(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
26
internal/handler/admin/user/getFamilyListHandler.go
Normal file
26
internal/handler/admin/user/getFamilyListHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Get family list
|
||||
func GetFamilyListHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.GetFamilyListRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewGetFamilyListLogic(c.Request.Context(), svcCtx)
|
||||
resp, err := l.GetFamilyList(&req)
|
||||
result.HttpResult(c, resp, err)
|
||||
}
|
||||
}
|
||||
26
internal/handler/admin/user/removeFamilyMemberHandler.go
Normal file
26
internal/handler/admin/user/removeFamilyMemberHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Remove family member
|
||||
func RemoveFamilyMemberHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.RemoveFamilyMemberRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewRemoveFamilyMemberLogic(c.Request.Context(), svcCtx)
|
||||
err := l.RemoveFamilyMember(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
26
internal/handler/admin/user/updateFamilyMaxMembersHandler.go
Normal file
26
internal/handler/admin/user/updateFamilyMaxMembersHandler.go
Normal file
@ -0,0 +1,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/perfect-panel/server/internal/logic/admin/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/result"
|
||||
)
|
||||
|
||||
// Update family max members
|
||||
func UpdateFamilyMaxMembersHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||
return func(c *gin.Context) {
|
||||
var req types.UpdateFamilyMaxMembersRequest
|
||||
_ = c.ShouldBind(&req)
|
||||
validateErr := svcCtx.Validate(&req)
|
||||
if validateErr != nil {
|
||||
result.ParamErrorResult(c, validateErr)
|
||||
return
|
||||
}
|
||||
|
||||
l := user.NewUpdateFamilyMaxMembersLogic(c.Request.Context(), svcCtx)
|
||||
err := l.UpdateFamilyMaxMembers(&req)
|
||||
result.HttpResult(c, nil, err)
|
||||
}
|
||||
}
|
||||
@ -623,6 +623,21 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
|
||||
|
||||
// Get user subcribe traffic logs
|
||||
adminUserGroupRouter.GET("/subscribe/traffic_logs", adminUser.GetUserSubscribeTrafficLogsHandler(serverCtx))
|
||||
|
||||
// Get family list
|
||||
adminUserGroupRouter.GET("/family/list", adminUser.GetFamilyListHandler(serverCtx))
|
||||
|
||||
// Get family detail
|
||||
adminUserGroupRouter.GET("/family/detail", adminUser.GetFamilyDetailHandler(serverCtx))
|
||||
|
||||
// Update family max members
|
||||
adminUserGroupRouter.PUT("/family/max_members", adminUser.UpdateFamilyMaxMembersHandler(serverCtx))
|
||||
|
||||
// Remove family member
|
||||
adminUserGroupRouter.PUT("/family/member/remove", adminUser.RemoveFamilyMemberHandler(serverCtx))
|
||||
|
||||
// Dissolve family
|
||||
adminUserGroupRouter.PUT("/family/dissolve", adminUser.DissolveFamilyHandler(serverCtx))
|
||||
}
|
||||
|
||||
authGroupRouter := router.Group("/v1/auth")
|
||||
|
||||
@ -3,11 +3,13 @@ package user
|
||||
import (
|
||||
"context"
|
||||
|
||||
userModel "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DeleteUserSubscribeLogic struct {
|
||||
@ -27,13 +29,17 @@ func NewDeleteUserSubscribeLogic(ctx context.Context, svcCtx *svc.ServiceContext
|
||||
|
||||
func (l *DeleteUserSubscribeLogic) DeleteUserSubscribe(req *types.DeleteUserSubscribeRequest) error {
|
||||
// find user subscribe by ID
|
||||
userSubscribe, err := l.svcCtx.UserModel.FindOneSubscribe(l.ctx, req.UserSubscribeId)
|
||||
userSubscribe := &userModel.Subscribe{}
|
||||
err := l.svcCtx.DB.WithContext(l.ctx).Model(&userModel.Subscribe{}).Where("id = ?", req.UserSubscribeId).First(userSubscribe).Error
|
||||
if err != nil {
|
||||
l.Errorw("failed to find user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InvalidParams, "user subscribe not found"), "failed to find user subscribe: %v", err.Error())
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "failed to find user subscribe: %v", err.Error())
|
||||
}
|
||||
|
||||
err = l.svcCtx.UserModel.DeleteSubscribeById(l.ctx, req.UserSubscribeId)
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).Where("id = ?", req.UserSubscribeId).Delete(&userModel.Subscribe{}).Error
|
||||
if err != nil {
|
||||
l.Errorw("failed to delete user subscribe", logger.Field("error", err.Error()), logger.Field("userSubscribeId", req.UserSubscribeId))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "failed to delete user subscribe: %v", err.Error())
|
||||
|
||||
68
internal/logic/admin/user/dissolveFamilyLogic.go
Normal file
68
internal/logic/admin/user/dissolveFamilyLogic.go
Normal file
@ -0,0 +1,68 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DissolveFamilyLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logger.Logger
|
||||
}
|
||||
|
||||
func NewDissolveFamilyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DissolveFamilyLogic {
|
||||
return &DissolveFamilyLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logger.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DissolveFamilyLogic) DissolveFamily(req *types.DissolveFamilyRequest) error {
|
||||
var family modelUser.UserFamily
|
||||
err := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Where("id = ? AND deleted_at IS NULL", req.FamilyId).
|
||||
First(&family).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyNotExist), "family does not exist")
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family failed")
|
||||
}
|
||||
if family.Status != modelUser.FamilyStatusActive {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyStatusInvalid), "family status is invalid")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
transactionErr := l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err = tx.Model(&modelUser.UserFamilyMember{}).
|
||||
Where("family_id = ? AND deleted_at IS NULL AND status = ?", req.FamilyId, modelUser.FamilyMemberActive).
|
||||
Updates(map[string]interface{}{
|
||||
"status": modelUser.FamilyMemberRemoved,
|
||||
"left_at": now,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "remove family members failed")
|
||||
}
|
||||
|
||||
if err = tx.Model(&modelUser.UserFamily{}).
|
||||
Where("id = ?", req.FamilyId).
|
||||
Update("status", 0).Error; err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable family failed")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
return transactionErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
99
internal/logic/admin/user/familyCommon.go
Normal file
99
internal/logic/admin/user/familyCommon.go
Normal file
@ -0,0 +1,99 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mapFamilyStatus(status uint8) string {
|
||||
if status == user.FamilyStatusActive {
|
||||
return "active"
|
||||
}
|
||||
return "disabled"
|
||||
}
|
||||
|
||||
func mapFamilyRoleName(role uint8) string {
|
||||
switch role {
|
||||
case user.FamilyRoleOwner:
|
||||
return "owner"
|
||||
case user.FamilyRoleMember:
|
||||
return "member"
|
||||
default:
|
||||
return fmt.Sprintf("role_%d", role)
|
||||
}
|
||||
}
|
||||
|
||||
func mapFamilyMemberStatusName(status uint8) string {
|
||||
switch status {
|
||||
case user.FamilyMemberActive:
|
||||
return "active"
|
||||
case user.FamilyMemberLeft:
|
||||
return "left"
|
||||
case user.FamilyMemberRemoved:
|
||||
return "removed"
|
||||
default:
|
||||
return fmt.Sprintf("status_%d", status)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeFamilyStatusInput(status string) (uint8, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(status)) {
|
||||
case "", "all":
|
||||
return 0, false
|
||||
case "active", "1":
|
||||
return user.FamilyStatusActive, true
|
||||
case "disabled", "0":
|
||||
return 0, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func findUserIdentifiers(ctx context.Context, db *gorm.DB, userIDs []int64) (map[int64]string, error) {
|
||||
identifierMap := make(map[int64]string)
|
||||
if len(userIDs) == 0 {
|
||||
return identifierMap, nil
|
||||
}
|
||||
|
||||
type authRow struct {
|
||||
UserId int64
|
||||
AuthType string
|
||||
AuthIdentifier string
|
||||
}
|
||||
|
||||
var rows []authRow
|
||||
err := db.WithContext(ctx).
|
||||
Table("user_auth_methods").
|
||||
Select("user_id, auth_type, auth_identifier").
|
||||
Where("user_id IN ? AND deleted_at IS NULL", userIDs).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priority := map[string]int{
|
||||
"email": 1,
|
||||
"mobile": 2,
|
||||
"telegram": 3,
|
||||
"device": 4,
|
||||
}
|
||||
selectedPriority := make(map[int64]int, len(userIDs))
|
||||
|
||||
for _, row := range rows {
|
||||
currentPriority := priority[row.AuthType]
|
||||
if currentPriority == 0 {
|
||||
currentPriority = 100
|
||||
}
|
||||
if previous, exists := selectedPriority[row.UserId]; exists && previous <= currentPriority {
|
||||
continue
|
||||
}
|
||||
selectedPriority[row.UserId] = currentPriority
|
||||
identifierMap[row.UserId] = row.AuthIdentifier
|
||||
}
|
||||
|
||||
return identifierMap, nil
|
||||
}
|
||||
106
internal/logic/admin/user/getFamilyDetailLogic.go
Normal file
106
internal/logic/admin/user/getFamilyDetailLogic.go
Normal file
@ -0,0 +1,106 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GetFamilyDetailLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logger.Logger
|
||||
}
|
||||
|
||||
func NewGetFamilyDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFamilyDetailLogic {
|
||||
return &GetFamilyDetailLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logger.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetFamilyDetailLogic) GetFamilyDetail(req *types.GetFamilyDetailRequest) (*types.FamilyDetail, error) {
|
||||
var family modelUser.UserFamily
|
||||
err := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Where("id = ? AND deleted_at IS NULL", req.Id).
|
||||
First(&family).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.FamilyNotExist), "family does not exist")
|
||||
}
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family detail failed")
|
||||
}
|
||||
|
||||
var members []modelUser.UserFamilyMember
|
||||
if err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Where("family_id = ? AND deleted_at IS NULL", family.Id).
|
||||
Order("joined_at DESC").
|
||||
Find(&members).Error; err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family members failed")
|
||||
}
|
||||
|
||||
userIDs := make([]int64, 0, len(members)+1)
|
||||
userIDs = append(userIDs, family.OwnerUserId)
|
||||
activeMemberCount := int64(0)
|
||||
for _, member := range members {
|
||||
userIDs = append(userIDs, member.UserId)
|
||||
if member.Status == modelUser.FamilyMemberActive {
|
||||
activeMemberCount++
|
||||
}
|
||||
}
|
||||
|
||||
identifierMap, identifierErr := findUserIdentifiers(l.ctx, l.svcCtx.DB, userIDs)
|
||||
if identifierErr != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family member identifiers failed")
|
||||
}
|
||||
|
||||
memberItems := make([]types.FamilyMemberItem, 0, len(members))
|
||||
for _, member := range members {
|
||||
identifier := identifierMap[member.UserId]
|
||||
if identifier == "" {
|
||||
identifier = strconv.FormatInt(member.UserId, 10)
|
||||
}
|
||||
|
||||
memberItem := types.FamilyMemberItem{
|
||||
UserId: member.UserId,
|
||||
Identifier: identifier,
|
||||
Role: member.Role,
|
||||
RoleName: mapFamilyRoleName(member.Role),
|
||||
Status: member.Status,
|
||||
StatusName: mapFamilyMemberStatusName(member.Status),
|
||||
JoinSource: member.JoinSource,
|
||||
JoinedAt: member.JoinedAt.Unix(),
|
||||
}
|
||||
if member.LeftAt != nil {
|
||||
memberItem.LeftAt = member.LeftAt.Unix()
|
||||
}
|
||||
memberItems = append(memberItems, memberItem)
|
||||
}
|
||||
|
||||
ownerIdentifier := identifierMap[family.OwnerUserId]
|
||||
if ownerIdentifier == "" {
|
||||
ownerIdentifier = strconv.FormatInt(family.OwnerUserId, 10)
|
||||
}
|
||||
|
||||
return &types.FamilyDetail{
|
||||
Summary: types.FamilySummary{
|
||||
FamilyId: family.Id,
|
||||
OwnerUserId: family.OwnerUserId,
|
||||
OwnerIdentifier: ownerIdentifier,
|
||||
Status: mapFamilyStatus(family.Status),
|
||||
ActiveMemberCount: activeMemberCount,
|
||||
MaxMembers: family.MaxMembers,
|
||||
CreatedAt: family.CreatedAt.Unix(),
|
||||
UpdatedAt: family.UpdatedAt.Unix(),
|
||||
},
|
||||
Members: memberItems,
|
||||
}, nil
|
||||
}
|
||||
147
internal/logic/admin/user/getFamilyListLogic.go
Normal file
147
internal/logic/admin/user/getFamilyListLogic.go
Normal file
@ -0,0 +1,147 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type GetFamilyListLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logger.Logger
|
||||
}
|
||||
|
||||
func NewGetFamilyListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFamilyListLogic {
|
||||
return &GetFamilyListLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logger.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *GetFamilyListLogic) GetFamilyList(req *types.GetFamilyListRequest) (*types.GetFamilyListResponse, error) {
|
||||
page := req.Page
|
||||
size := req.Size
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
if size > 200 {
|
||||
size = 200
|
||||
}
|
||||
|
||||
query := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Model(&modelUser.UserFamily{}).
|
||||
Where("user_family.deleted_at IS NULL")
|
||||
|
||||
if req.OwnerUserId != nil {
|
||||
query = query.Where("user_family.owner_user_id = ?", *req.OwnerUserId)
|
||||
}
|
||||
if req.FamilyId != nil {
|
||||
query = query.Where("user_family.id = ?", *req.FamilyId)
|
||||
}
|
||||
if req.UserId != nil {
|
||||
query = query.Where(
|
||||
"EXISTS (SELECT 1 FROM user_family_member ufm WHERE ufm.family_id = user_family.id AND ufm.deleted_at IS NULL AND ufm.status = ? AND ufm.user_id = ?)",
|
||||
modelUser.FamilyMemberActive, *req.UserId,
|
||||
)
|
||||
}
|
||||
if statusValue, ok := normalizeFamilyStatusInput(req.Status); ok {
|
||||
query = query.Where("user_family.status = ?", statusValue)
|
||||
}
|
||||
|
||||
keyword := strings.TrimSpace(req.Keyword)
|
||||
if keyword != "" {
|
||||
keywordLike := "%" + keyword + "%"
|
||||
query = query.Where(
|
||||
"(CAST(user_family.id AS CHAR) LIKE ? OR CAST(user_family.owner_user_id AS CHAR) LIKE ? OR "+
|
||||
"EXISTS (SELECT 1 FROM user_auth_methods owner_auth WHERE owner_auth.user_id = user_family.owner_user_id AND owner_auth.deleted_at IS NULL AND owner_auth.auth_identifier LIKE ?) OR "+
|
||||
"EXISTS (SELECT 1 FROM user_family_member keyword_member JOIN user_auth_methods keyword_auth ON keyword_auth.user_id = keyword_member.user_id AND keyword_auth.deleted_at IS NULL WHERE keyword_member.family_id = user_family.id AND keyword_member.deleted_at IS NULL AND keyword_auth.auth_identifier LIKE ?))",
|
||||
keywordLike, keywordLike, keywordLike, keywordLike,
|
||||
)
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count family list failed")
|
||||
}
|
||||
|
||||
var families []modelUser.UserFamily
|
||||
if err := query.Order("user_family.id DESC").
|
||||
Limit(size).
|
||||
Offset((page - 1) * size).
|
||||
Find(&families).Error; err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family list failed")
|
||||
}
|
||||
|
||||
if len(families) == 0 {
|
||||
return &types.GetFamilyListResponse{
|
||||
Total: total,
|
||||
List: []types.FamilySummary{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
ownerIDs := make([]int64, 0, len(families))
|
||||
familyIDs := make([]int64, 0, len(families))
|
||||
for _, family := range families {
|
||||
ownerIDs = append(ownerIDs, family.OwnerUserId)
|
||||
familyIDs = append(familyIDs, family.Id)
|
||||
}
|
||||
|
||||
identifierMap, identifierErr := findUserIdentifiers(l.ctx, l.svcCtx.DB, ownerIDs)
|
||||
if identifierErr != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family owner identifiers failed")
|
||||
}
|
||||
|
||||
type familyCount struct {
|
||||
FamilyId int64
|
||||
Count int64
|
||||
}
|
||||
var counts []familyCount
|
||||
if err := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_family_member").
|
||||
Select("family_id, COUNT(1) as count").
|
||||
Where("family_id IN ? AND deleted_at IS NULL AND status = ?", familyIDs, modelUser.FamilyMemberActive).
|
||||
Group("family_id").
|
||||
Scan(&counts).Error; err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family member counts failed")
|
||||
}
|
||||
countMap := make(map[int64]int64, len(counts))
|
||||
for _, count := range counts {
|
||||
countMap[count.FamilyId] = count.Count
|
||||
}
|
||||
|
||||
list := make([]types.FamilySummary, 0, len(families))
|
||||
for _, family := range families {
|
||||
ownerIdentifier := identifierMap[family.OwnerUserId]
|
||||
if ownerIdentifier == "" {
|
||||
ownerIdentifier = strconv.FormatInt(family.OwnerUserId, 10)
|
||||
}
|
||||
|
||||
list = append(list, types.FamilySummary{
|
||||
FamilyId: family.Id,
|
||||
OwnerUserId: family.OwnerUserId,
|
||||
OwnerIdentifier: ownerIdentifier,
|
||||
Status: mapFamilyStatus(family.Status),
|
||||
ActiveMemberCount: countMap[family.Id],
|
||||
MaxMembers: family.MaxMembers,
|
||||
CreatedAt: family.CreatedAt.Unix(),
|
||||
UpdatedAt: family.UpdatedAt.Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
return &types.GetFamilyListResponse{
|
||||
Total: total,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
@ -3,6 +3,7 @@ package user
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
@ -32,5 +33,51 @@ func (l *GetUserDetailLogic) GetUserDetail(req *types.GetDetailRequest) (*types.
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user detail error: %v", err.Error())
|
||||
}
|
||||
tool.DeepCopy(&resp, userInfo)
|
||||
|
||||
type familyRelation struct {
|
||||
FamilyId int64
|
||||
Role uint8
|
||||
FamilyStatus uint8
|
||||
OwnerUserId int64
|
||||
MaxMembers int64
|
||||
}
|
||||
|
||||
var relation familyRelation
|
||||
relationErr := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_family_member").
|
||||
Select("user_family_member.family_id, user_family_member.role, user_family.status as family_status, user_family.owner_user_id, user_family.max_members").
|
||||
Joins("JOIN user_family ON user_family.id = user_family_member.family_id AND user_family.deleted_at IS NULL").
|
||||
Where("user_family_member.user_id = ? AND user_family_member.deleted_at IS NULL AND user_family_member.status = ?", req.Id, user.FamilyMemberActive).
|
||||
First(&relation).Error
|
||||
if relationErr == nil {
|
||||
resp.FamilyJoined = true
|
||||
resp.FamilyId = relation.FamilyId
|
||||
resp.FamilyRole = relation.Role
|
||||
resp.FamilyOwnerUserId = relation.OwnerUserId
|
||||
resp.FamilyMaxMembers = relation.MaxMembers
|
||||
if relation.FamilyStatus == user.FamilyStatusActive {
|
||||
resp.FamilyStatus = "active"
|
||||
} else {
|
||||
resp.FamilyStatus = "disabled"
|
||||
}
|
||||
if relation.Role == user.FamilyRoleOwner {
|
||||
resp.FamilyRoleName = "owner"
|
||||
} else if relation.Role == user.FamilyRoleMember {
|
||||
resp.FamilyRoleName = "member"
|
||||
}
|
||||
|
||||
type familyCount struct {
|
||||
Count int64
|
||||
}
|
||||
var count familyCount
|
||||
if countErr := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_family_member").
|
||||
Select("COUNT(1) as count").
|
||||
Where("family_id = ? AND status = ? AND deleted_at IS NULL", relation.FamilyId, user.FamilyMemberActive).
|
||||
Scan(&count).Error; countErr == nil {
|
||||
resp.FamilyMemberCount = count.Count
|
||||
}
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@ -34,6 +35,10 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
SubscribeId: req.SubscribeId,
|
||||
UserSubscribeId: req.UserSubscribeId,
|
||||
ShortCode: req.ShortCode,
|
||||
FamilyJoined: req.FamilyJoined,
|
||||
FamilyStatus: req.FamilyStatus,
|
||||
FamilyOwnerUserId: req.FamilyOwnerUserId,
|
||||
FamilyId: req.FamilyId,
|
||||
Order: "DESC",
|
||||
})
|
||||
if err != nil {
|
||||
@ -51,6 +56,64 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
l.Logger.Error("FindActiveSubscribesByUserIds failed", logger.Field("error", err.Error()))
|
||||
}
|
||||
|
||||
type familyRelation struct {
|
||||
UserId int64
|
||||
FamilyId int64
|
||||
Role uint8
|
||||
FamilyStatus uint8
|
||||
OwnerUserId int64
|
||||
MaxMembers int64
|
||||
}
|
||||
|
||||
relationMap := map[int64]familyRelation{}
|
||||
familyIds := make([]int64, 0)
|
||||
if len(userIds) > 0 {
|
||||
var relations []familyRelation
|
||||
relationErr := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_family_member").
|
||||
Select("user_family_member.user_id, user_family_member.family_id, user_family_member.role, user_family.status as family_status, user_family.owner_user_id, user_family.max_members").
|
||||
Joins("JOIN user_family ON user_family.id = user_family_member.family_id AND user_family.deleted_at IS NULL").
|
||||
Where("user_family_member.user_id IN ? AND user_family_member.deleted_at IS NULL AND user_family_member.status = ?", userIds, user.FamilyMemberActive).
|
||||
Scan(&relations).Error
|
||||
if relationErr != nil {
|
||||
l.Logger.Error("query family relations failed", logger.Field("error", relationErr.Error()))
|
||||
}
|
||||
|
||||
familyIdSet := make(map[int64]struct{}, len(relations))
|
||||
for _, relation := range relations {
|
||||
if _, exists := relationMap[relation.UserId]; !exists {
|
||||
relationMap[relation.UserId] = relation
|
||||
}
|
||||
if _, ok := familyIdSet[relation.FamilyId]; ok {
|
||||
continue
|
||||
}
|
||||
familyIdSet[relation.FamilyId] = struct{}{}
|
||||
familyIds = append(familyIds, relation.FamilyId)
|
||||
}
|
||||
}
|
||||
|
||||
type familyCount struct {
|
||||
FamilyId int64
|
||||
Count int64
|
||||
}
|
||||
|
||||
familyCountMap := map[int64]int64{}
|
||||
if len(familyIds) > 0 {
|
||||
var counts []familyCount
|
||||
countErr := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_family_member").
|
||||
Select("family_id, COUNT(1) as count").
|
||||
Where("family_id IN ? AND status = ? AND deleted_at IS NULL", familyIds, user.FamilyMemberActive).
|
||||
Group("family_id").
|
||||
Scan(&counts).Error
|
||||
if countErr != nil {
|
||||
l.Logger.Error("query family member count failed", logger.Field("error", countErr.Error()))
|
||||
}
|
||||
for _, count := range counts {
|
||||
familyCountMap[count.FamilyId] = count.Count
|
||||
}
|
||||
}
|
||||
|
||||
userRespList := make([]types.User, 0, len(list))
|
||||
|
||||
for _, item := range list {
|
||||
@ -66,6 +129,7 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
if activeSubs != nil {
|
||||
if info, ok := activeSubs[item.Id]; ok {
|
||||
u.MemberStatus = info.MemberStatus
|
||||
u.PurchasedPackage = info.PurchasedPackage
|
||||
if info.LastTrafficAt != nil {
|
||||
trafficTime := info.LastTrafficAt.Unix()
|
||||
if trafficTime > u.LastLoginTime {
|
||||
@ -85,6 +149,29 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
}
|
||||
u.AuthMethods = authMethods
|
||||
|
||||
if relation, ok := relationMap[item.Id]; ok {
|
||||
u.FamilyJoined = true
|
||||
u.FamilyId = relation.FamilyId
|
||||
u.FamilyRole = relation.Role
|
||||
u.FamilyOwnerUserId = relation.OwnerUserId
|
||||
u.FamilyMaxMembers = relation.MaxMembers
|
||||
u.FamilyMemberCount = familyCountMap[relation.FamilyId]
|
||||
switch relation.FamilyStatus {
|
||||
case user.FamilyStatusActive:
|
||||
u.FamilyStatus = "active"
|
||||
default:
|
||||
u.FamilyStatus = "disabled"
|
||||
}
|
||||
switch relation.Role {
|
||||
case user.FamilyRoleOwner:
|
||||
u.FamilyRoleName = "owner"
|
||||
case user.FamilyRoleMember:
|
||||
u.FamilyRoleName = "member"
|
||||
default:
|
||||
u.FamilyRoleName = fmt.Sprintf("role_%d", relation.Role)
|
||||
}
|
||||
}
|
||||
|
||||
userRespList = append(userRespList, u)
|
||||
}
|
||||
|
||||
|
||||
71
internal/logic/admin/user/removeFamilyMemberLogic.go
Normal file
71
internal/logic/admin/user/removeFamilyMemberLogic.go
Normal file
@ -0,0 +1,71 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RemoveFamilyMemberLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logger.Logger
|
||||
}
|
||||
|
||||
func NewRemoveFamilyMemberLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RemoveFamilyMemberLogic {
|
||||
return &RemoveFamilyMemberLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logger.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *RemoveFamilyMemberLogic) RemoveFamilyMember(req *types.RemoveFamilyMemberRequest) error {
|
||||
var family modelUser.UserFamily
|
||||
err := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Where("id = ? AND deleted_at IS NULL", req.FamilyId).
|
||||
First(&family).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyNotExist), "family does not exist")
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family failed")
|
||||
}
|
||||
if family.Status != modelUser.FamilyStatusActive {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyStatusInvalid), "family status is invalid")
|
||||
}
|
||||
|
||||
var member modelUser.UserFamilyMember
|
||||
err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Where("family_id = ? AND user_id = ? AND deleted_at IS NULL AND status = ?", req.FamilyId, req.UserId, modelUser.FamilyMemberActive).
|
||||
First(&member).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyNotExist), "active family member does not exist")
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family member failed")
|
||||
}
|
||||
if member.Role == modelUser.FamilyRoleOwner {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyOwnerOperationForbidden), "cannot remove family owner")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Model(&modelUser.UserFamilyMember{}).
|
||||
Where("id = ?", member.Id).
|
||||
Updates(map[string]interface{}{
|
||||
"status": modelUser.FamilyMemberRemoved,
|
||||
"left_at": now,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "remove family member failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
60
internal/logic/admin/user/updateFamilyMaxMembersLogic.go
Normal file
60
internal/logic/admin/user/updateFamilyMaxMembersLogic.go
Normal file
@ -0,0 +1,60 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UpdateFamilyMaxMembersLogic struct {
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
logger.Logger
|
||||
}
|
||||
|
||||
func NewUpdateFamilyMaxMembersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateFamilyMaxMembersLogic {
|
||||
return &UpdateFamilyMaxMembersLogic{
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
Logger: logger.WithContext(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *UpdateFamilyMaxMembersLogic) UpdateFamilyMaxMembers(req *types.UpdateFamilyMaxMembersRequest) error {
|
||||
var family modelUser.UserFamily
|
||||
err := l.svcCtx.DB.WithContext(l.ctx).
|
||||
Where("id = ? AND deleted_at IS NULL", req.FamilyId).
|
||||
First(&family).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyNotExist), "family does not exist")
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query family failed")
|
||||
}
|
||||
|
||||
var activeCount int64
|
||||
if err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Table("user_family_member").
|
||||
Where("family_id = ? AND deleted_at IS NULL AND status = ?", req.FamilyId, modelUser.FamilyMemberActive).
|
||||
Count(&activeCount).Error; err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "count active members failed")
|
||||
}
|
||||
if req.MaxMembers < activeCount {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.FamilyMemberLimitExceeded), "max members below active member count")
|
||||
}
|
||||
|
||||
if err = l.svcCtx.DB.WithContext(l.ctx).
|
||||
Model(&modelUser.UserFamily{}).
|
||||
Where("id = ?", req.FamilyId).
|
||||
Update("max_members", req.MaxMembers).Error; err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update family max members failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
63
internal/logic/common/subscribeModeRoute.go
Normal file
63
internal/logic/common/subscribeModeRoute.go
Normal file
@ -0,0 +1,63 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
)
|
||||
|
||||
type PurchaseRoute string
|
||||
|
||||
const (
|
||||
PurchaseRouteNewPurchase PurchaseRoute = "new_purchase"
|
||||
PurchaseRoutePurchaseToRenewal PurchaseRoute = "purchase_to_renewal"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSingleModePlanMismatch = errors.New("single subscribe mode plan mismatch")
|
||||
)
|
||||
|
||||
type PurchaseRouteDecision struct {
|
||||
SingleMode bool
|
||||
Route PurchaseRoute
|
||||
ResolvedSubscribeID int64
|
||||
Anchor *user.Subscribe
|
||||
}
|
||||
|
||||
type FindSingleModeAnchorFunc func(ctx context.Context, userID int64) (*user.Subscribe, error)
|
||||
|
||||
func ResolvePurchaseRoute(
|
||||
ctx context.Context,
|
||||
singleMode bool,
|
||||
userID int64,
|
||||
requestedSubscribeID int64,
|
||||
findAnchor FindSingleModeAnchorFunc,
|
||||
) (*PurchaseRouteDecision, error) {
|
||||
decision := &PurchaseRouteDecision{
|
||||
SingleMode: singleMode,
|
||||
Route: PurchaseRouteNewPurchase,
|
||||
ResolvedSubscribeID: requestedSubscribeID,
|
||||
}
|
||||
|
||||
if !singleMode || userID == 0 || findAnchor == nil {
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
anchorSub, err := findAnchor(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if anchorSub == nil {
|
||||
return decision, nil
|
||||
}
|
||||
|
||||
if requestedSubscribeID != anchorSub.SubscribeId {
|
||||
return nil, ErrSingleModePlanMismatch
|
||||
}
|
||||
|
||||
decision.Route = PurchaseRoutePurchaseToRenewal
|
||||
decision.ResolvedSubscribeID = anchorSub.SubscribeId
|
||||
decision.Anchor = anchorSub
|
||||
return decision, nil
|
||||
}
|
||||
82
internal/logic/common/subscribeModeRoute_test.go
Normal file
82
internal/logic/common/subscribeModeRoute_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestResolvePurchaseRoute(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("single mode disabled", func(t *testing.T) {
|
||||
called := false
|
||||
decision, err := ResolvePurchaseRoute(ctx, false, 1, 100, func(ctx context.Context, userID int64) (*user.Subscribe, error) {
|
||||
called = true
|
||||
return nil, nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decision)
|
||||
require.Equal(t, PurchaseRouteNewPurchase, decision.Route)
|
||||
require.Equal(t, int64(100), decision.ResolvedSubscribeID)
|
||||
require.False(t, called)
|
||||
})
|
||||
|
||||
t.Run("single mode but empty user", func(t *testing.T) {
|
||||
decision, err := ResolvePurchaseRoute(ctx, true, 0, 100, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decision)
|
||||
require.Equal(t, PurchaseRouteNewPurchase, decision.Route)
|
||||
require.Equal(t, int64(100), decision.ResolvedSubscribeID)
|
||||
})
|
||||
|
||||
t.Run("single mode no anchor", func(t *testing.T) {
|
||||
decision, err := ResolvePurchaseRoute(ctx, true, 1, 100, func(ctx context.Context, userID int64) (*user.Subscribe, error) {
|
||||
return nil, nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decision)
|
||||
require.Equal(t, PurchaseRouteNewPurchase, decision.Route)
|
||||
require.Equal(t, int64(100), decision.ResolvedSubscribeID)
|
||||
})
|
||||
|
||||
t.Run("single mode routed to renewal", func(t *testing.T) {
|
||||
decision, err := ResolvePurchaseRoute(ctx, true, 1, 100, func(ctx context.Context, userID int64) (*user.Subscribe, error) {
|
||||
return &user.Subscribe{
|
||||
Id: 11,
|
||||
SubscribeId: 100,
|
||||
OrderId: 7,
|
||||
Token: "token",
|
||||
}, nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, decision)
|
||||
require.Equal(t, PurchaseRoutePurchaseToRenewal, decision.Route)
|
||||
require.Equal(t, int64(100), decision.ResolvedSubscribeID)
|
||||
require.NotNil(t, decision.Anchor)
|
||||
require.Equal(t, int64(11), decision.Anchor.Id)
|
||||
})
|
||||
|
||||
t.Run("single mode plan mismatch", func(t *testing.T) {
|
||||
decision, err := ResolvePurchaseRoute(ctx, true, 1, 100, func(ctx context.Context, userID int64) (*user.Subscribe, error) {
|
||||
return &user.Subscribe{
|
||||
Id: 11,
|
||||
SubscribeId: 200,
|
||||
}, nil
|
||||
})
|
||||
require.ErrorIs(t, err, ErrSingleModePlanMismatch)
|
||||
require.Nil(t, decision)
|
||||
})
|
||||
|
||||
t.Run("single mode anchor query error", func(t *testing.T) {
|
||||
queryErr := errors.New("query failed")
|
||||
decision, err := ResolvePurchaseRoute(ctx, true, 1, 100, func(ctx context.Context, userID int64) (*user.Subscribe, error) {
|
||||
return nil, queryErr
|
||||
})
|
||||
require.ErrorIs(t, err, queryErr)
|
||||
require.Nil(t, decision)
|
||||
})
|
||||
}
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
|
||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
|
||||
@ -52,25 +53,30 @@ func (l *PreCreateOrderLogic) PreCreateOrder(req *types.PurchaseOrderRequest) (r
|
||||
|
||||
targetSubscribeID := req.SubscribeId
|
||||
isSingleModeRenewal := false
|
||||
if l.svcCtx.Config.Subscribe.SingleModel {
|
||||
anchorSub, anchorErr := l.svcCtx.UserModel.FindSingleModeAnchorSubscribe(l.ctx, u.Id)
|
||||
decision, routeErr := commonLogic.ResolvePurchaseRoute(
|
||||
l.ctx,
|
||||
l.svcCtx.Config.Subscribe.SingleModel,
|
||||
u.Id,
|
||||
req.SubscribeId,
|
||||
l.svcCtx.UserModel.FindSingleModeAnchorSubscribe,
|
||||
)
|
||||
switch {
|
||||
case anchorErr == nil && anchorSub != nil:
|
||||
targetSubscribeID = anchorSub.SubscribeId
|
||||
isSingleModeRenewal = true
|
||||
if req.SubscribeId != targetSubscribeID {
|
||||
case errors.Is(routeErr, commonLogic.ErrSingleModePlanMismatch):
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribePlanMismatch), "single subscribe mode plan mismatch")
|
||||
}
|
||||
case errors.Is(routeErr, gorm.ErrRecordNotFound):
|
||||
case routeErr != nil:
|
||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", routeErr.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find single mode anchor subscribe error: %v", routeErr.Error())
|
||||
case decision != nil:
|
||||
targetSubscribeID = decision.ResolvedSubscribeID
|
||||
isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal
|
||||
if isSingleModeRenewal && decision.Anchor != nil {
|
||||
l.Infow("[PreCreateOrder] single mode purchase routed to renewal preview",
|
||||
logger.Field("mode", "single"),
|
||||
logger.Field("route", "purchase_to_renewal"),
|
||||
logger.Field("anchor_user_subscribe_id", anchorSub.Id),
|
||||
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
|
||||
logger.Field("user_id", u.Id),
|
||||
)
|
||||
case errors.Is(anchorErr, gorm.ErrRecordNotFound):
|
||||
case anchorErr != nil:
|
||||
l.Errorw("[PreCreateOrder] Database query error", logger.Field("error", anchorErr.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find single mode anchor subscribe error: %v", anchorErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||
"github.com/perfect-panel/server/internal/model/log"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
|
||||
@ -71,30 +72,35 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
||||
subscribeToken := ""
|
||||
anchorUserSubscribeID := int64(0)
|
||||
isSingleModeRenewal := false
|
||||
if l.svcCtx.Config.Subscribe.SingleModel {
|
||||
anchorSub, anchorErr := l.svcCtx.UserModel.FindSingleModeAnchorSubscribe(l.ctx, u.Id)
|
||||
decision, routeErr := commonLogic.ResolvePurchaseRoute(
|
||||
l.ctx,
|
||||
l.svcCtx.Config.Subscribe.SingleModel,
|
||||
u.Id,
|
||||
req.SubscribeId,
|
||||
l.svcCtx.UserModel.FindSingleModeAnchorSubscribe,
|
||||
)
|
||||
switch {
|
||||
case anchorErr == nil && anchorSub != nil:
|
||||
if req.SubscribeId != anchorSub.SubscribeId {
|
||||
case errors.Is(routeErr, commonLogic.ErrSingleModePlanMismatch):
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribePlanMismatch), "single subscribe mode plan mismatch")
|
||||
}
|
||||
targetSubscribeID = anchorSub.SubscribeId
|
||||
case errors.Is(routeErr, gorm.ErrRecordNotFound):
|
||||
case routeErr != nil:
|
||||
l.Errorw("[Purchase] Database query error", logger.Field("error", routeErr.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find single mode anchor subscribe error: %v", routeErr.Error())
|
||||
case decision != nil:
|
||||
targetSubscribeID = decision.ResolvedSubscribeID
|
||||
isSingleModeRenewal = decision.Route == commonLogic.PurchaseRoutePurchaseToRenewal
|
||||
if isSingleModeRenewal && decision.Anchor != nil {
|
||||
orderType = 2
|
||||
parentOrderID = anchorSub.OrderId
|
||||
subscribeToken = anchorSub.Token
|
||||
anchorUserSubscribeID = anchorSub.Id
|
||||
isSingleModeRenewal = true
|
||||
parentOrderID = decision.Anchor.OrderId
|
||||
subscribeToken = decision.Anchor.Token
|
||||
anchorUserSubscribeID = decision.Anchor.Id
|
||||
l.Infow("[Purchase] single mode purchase routed to renewal",
|
||||
logger.Field("mode", "single"),
|
||||
logger.Field("route", "purchase_to_renewal"),
|
||||
logger.Field("anchor_user_subscribe_id", anchorSub.Id),
|
||||
logger.Field("anchor_user_subscribe_id", decision.Anchor.Id),
|
||||
logger.Field("order_no", "pending"),
|
||||
logger.Field("user_id", u.Id),
|
||||
)
|
||||
case errors.Is(anchorErr, gorm.ErrRecordNotFound):
|
||||
case anchorErr != nil:
|
||||
l.Errorw("[Purchase] Database query error", logger.Field("error", anchorErr.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find single mode anchor subscribe error: %v", anchorErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,9 @@ func (l *PurchaseLogic) Purchase(req *types.PortalPurchaseRequest) (resp *types.
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user auth error: %v", err.Error())
|
||||
}
|
||||
if userAuth.UserId != 0 {
|
||||
if l.svcCtx.Config.Subscribe.SingleModel {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribeModeExceedsLimit), "single subscribe mode exceeds limit")
|
||||
}
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user already exists")
|
||||
}
|
||||
// find subscribe plan
|
||||
|
||||
@ -71,6 +71,7 @@ func (s *Subscribe) GetCacheKeys() []string {
|
||||
}
|
||||
if s.UserId != 0 {
|
||||
keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeUserPrefix, s.UserId))
|
||||
keys = append(keys, fmt.Sprintf("%s%d:all", cacheUserSubscribeUserPrefix, s.UserId))
|
||||
}
|
||||
if s.Id != 0 {
|
||||
keys = append(keys, fmt.Sprintf("%s%d", cacheUserSubscribeIdPrefix, s.Id))
|
||||
|
||||
@ -3,6 +3,7 @@ package user
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/model/order"
|
||||
@ -62,6 +63,10 @@ type UserFilterParams struct {
|
||||
SubscribeId *int64
|
||||
UserSubscribeId *int64
|
||||
ShortCode string
|
||||
FamilyJoined *bool
|
||||
FamilyStatus string
|
||||
FamilyOwnerUserId *int64
|
||||
FamilyId *int64
|
||||
Order string // Order by id, e.g., "desc"
|
||||
Unscoped bool // Whether to include soft-deleted records
|
||||
}
|
||||
@ -119,6 +124,7 @@ type customUserLogicModel interface {
|
||||
type UserStatusInfo struct {
|
||||
MemberStatus string
|
||||
LastTrafficAt *time.Time
|
||||
PurchasedPackage string
|
||||
}
|
||||
|
||||
type UserStatisticsWithDate struct {
|
||||
@ -140,6 +146,26 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
|
||||
var list []*User
|
||||
var total int64
|
||||
err := m.QueryNoCacheCtx(ctx, &list, func(conn *gorm.DB, v interface{}) error {
|
||||
joinedFamily := false
|
||||
joinFamily := func(c *gorm.DB) *gorm.DB {
|
||||
if joinedFamily {
|
||||
return c
|
||||
}
|
||||
joinedFamily = true
|
||||
return c.
|
||||
Joins("JOIN user_family_member ufm ON ufm.user_id = user.id AND ufm.deleted_at IS NULL AND ufm.status = ?", FamilyMemberActive).
|
||||
Joins("JOIN user_family uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL")
|
||||
}
|
||||
joinFamilyLeft := func(c *gorm.DB) *gorm.DB {
|
||||
if joinedFamily {
|
||||
return c
|
||||
}
|
||||
joinedFamily = true
|
||||
return c.
|
||||
Joins("LEFT JOIN user_family_member ufm ON ufm.user_id = user.id AND ufm.deleted_at IS NULL AND ufm.status = ?", FamilyMemberActive).
|
||||
Joins("LEFT JOIN user_family uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL")
|
||||
}
|
||||
|
||||
if filter != nil {
|
||||
if filter.UserId != nil {
|
||||
conn = conn.Where("user.id =?", *filter.UserId)
|
||||
@ -160,6 +186,46 @@ func (m *customUserModel) QueryPageList(ctx context.Context, page, size int, fil
|
||||
conn = conn.Joins("LEFT JOIN user_device ON user.id = user_device.user_id").
|
||||
Where("user_device.short_code LIKE ?", "%"+filter.ShortCode+"%")
|
||||
}
|
||||
|
||||
if filter.FamilyJoined != nil {
|
||||
if *filter.FamilyJoined {
|
||||
conn = joinFamily(conn)
|
||||
} else {
|
||||
conn = joinFamilyLeft(conn)
|
||||
conn = conn.Where("ufm.id IS NULL")
|
||||
}
|
||||
}
|
||||
|
||||
if filter.FamilyOwnerUserId != nil {
|
||||
conn = joinFamily(conn)
|
||||
conn = conn.Where("uf.owner_user_id = ?", *filter.FamilyOwnerUserId)
|
||||
}
|
||||
|
||||
if filter.FamilyId != nil {
|
||||
conn = joinFamily(conn)
|
||||
conn = conn.Where("uf.id = ?", *filter.FamilyId)
|
||||
}
|
||||
|
||||
if filter.FamilyStatus != "" {
|
||||
normalizedStatus := strings.ToLower(strings.TrimSpace(filter.FamilyStatus))
|
||||
var (
|
||||
statusValue uint8
|
||||
hasStatus bool
|
||||
)
|
||||
switch normalizedStatus {
|
||||
case "active", "1":
|
||||
statusValue = FamilyStatusActive
|
||||
hasStatus = true
|
||||
case "disabled", "0":
|
||||
statusValue = 0
|
||||
hasStatus = true
|
||||
}
|
||||
if hasStatus {
|
||||
conn = joinFamily(conn)
|
||||
conn = conn.Where("uf.status = ?", statusValue)
|
||||
}
|
||||
}
|
||||
|
||||
if filter.Order != "" {
|
||||
conn = conn.Order(fmt.Sprintf("user.id %s", filter.Order))
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@ -16,17 +18,19 @@ func (m *customUserModel) FindActiveSubscribesByUserIds(ctx context.Context, use
|
||||
type Result struct {
|
||||
UserId int64
|
||||
Name string
|
||||
Quantity int64
|
||||
UpdatedAt *time.Time
|
||||
}
|
||||
var results []Result
|
||||
|
||||
// Query latest active subscription for each user
|
||||
err := m.QueryNoCacheCtx(ctx, &results, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Table("user_subscribe").
|
||||
Select("user_subscribe.user_id, subscribe.name, user_subscribe.updated_at").
|
||||
Joins("LEFT JOIN subscribe ON user_subscribe.subscribe_id = subscribe.id").
|
||||
Where("user_subscribe.user_id IN ? AND user_subscribe.status IN (0, 1) AND user_subscribe.expire_time > ?", userIds, time.Now()).
|
||||
Order("user_subscribe.created_at ASC"). // Ascending so we can overwrite in map to get the latest
|
||||
return conn.Table("user_subscribe us").
|
||||
Select("us.user_id, subscribe.name, COALESCE(o.quantity, 1) AS quantity, us.updated_at").
|
||||
Joins("LEFT JOIN subscribe ON us.subscribe_id = subscribe.id").
|
||||
Joins("LEFT JOIN `order` o ON o.id = (SELECT MAX(o2.id) FROM `order` o2 WHERE o2.user_id = us.user_id AND o2.subscribe_id = us.subscribe_id AND o2.status IN (2, 5))").
|
||||
Where("us.user_id IN ? AND us.status IN (0, 1) AND us.expire_time > ?", userIds, time.Now()).
|
||||
Order("us.created_at ASC, us.id ASC"). // Ascending so we can overwrite in map to get the latest
|
||||
Scan(v).Error
|
||||
})
|
||||
|
||||
@ -35,11 +39,57 @@ func (m *customUserModel) FindActiveSubscribesByUserIds(ctx context.Context, use
|
||||
}
|
||||
|
||||
userMap := make(map[int64]*UserStatusInfo)
|
||||
packageTotals := make(map[int64]map[string]int64)
|
||||
packageOrder := make(map[int64][]string)
|
||||
for _, r := range results {
|
||||
userMap[r.UserId] = &UserStatusInfo{
|
||||
MemberStatus: r.Name,
|
||||
LastTrafficAt: r.UpdatedAt,
|
||||
name := strings.TrimSpace(r.Name)
|
||||
if name == "" {
|
||||
name = "Unknown"
|
||||
}
|
||||
|
||||
quantity := r.Quantity
|
||||
if quantity <= 0 {
|
||||
quantity = 1
|
||||
}
|
||||
|
||||
if _, ok := packageTotals[r.UserId]; !ok {
|
||||
packageTotals[r.UserId] = make(map[string]int64)
|
||||
}
|
||||
if _, exists := packageTotals[r.UserId][name]; !exists {
|
||||
packageOrder[r.UserId] = append(packageOrder[r.UserId], name)
|
||||
}
|
||||
packageTotals[r.UserId][name] += quantity
|
||||
|
||||
info, ok := userMap[r.UserId]
|
||||
if !ok {
|
||||
info = &UserStatusInfo{}
|
||||
userMap[r.UserId] = info
|
||||
}
|
||||
info.MemberStatus = formatPackageDisplay(name, quantity)
|
||||
if r.UpdatedAt != nil && (info.LastTrafficAt == nil || r.UpdatedAt.After(*info.LastTrafficAt)) {
|
||||
info.LastTrafficAt = r.UpdatedAt
|
||||
}
|
||||
}
|
||||
|
||||
for userID, info := range userMap {
|
||||
orderedNames := packageOrder[userID]
|
||||
if len(orderedNames) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := make([]string, 0, len(orderedNames))
|
||||
for _, name := range orderedNames {
|
||||
parts = append(parts, formatPackageDisplay(name, packageTotals[userID][name]))
|
||||
}
|
||||
info.PurchasedPackage = strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
return userMap, nil
|
||||
}
|
||||
|
||||
func formatPackageDisplay(name string, quantity int64) string {
|
||||
if quantity <= 1 {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("%s*%d", name, quantity)
|
||||
}
|
||||
|
||||
@ -9,6 +9,19 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (m *defaultUserModel) execSubscribeMutation(ctx context.Context, cacheModels []*Subscribe, execFn func(conn *gorm.DB) error, tx ...*gorm.DB) error {
|
||||
defer func() {
|
||||
_ = m.ClearSubscribeCacheByModels(ctx, cacheModels...)
|
||||
}()
|
||||
|
||||
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return execFn(conn)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) UpdateUserSubscribeCache(ctx context.Context, data *Subscribe) error {
|
||||
return m.ClearSubscribeCacheByModels(ctx, data)
|
||||
}
|
||||
@ -146,19 +159,9 @@ func (m *defaultUserModel) UpdateSubscribe(ctx context.Context, data *Subscribe,
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用 defer 确保更新后清理缓存
|
||||
defer func() {
|
||||
if clearErr := m.ClearSubscribeCacheByModels(ctx, old, data); clearErr != nil {
|
||||
// 记录清理缓存错误
|
||||
}
|
||||
}()
|
||||
|
||||
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return m.execSubscribeMutation(ctx, []*Subscribe{old, data}, func(conn *gorm.DB) error {
|
||||
return conn.Model(&Subscribe{}).Where("id = ?", data.Id).Save(data).Error
|
||||
})
|
||||
}, tx...)
|
||||
}
|
||||
|
||||
// DeleteSubscribe deletes a record.
|
||||
@ -168,36 +171,16 @@ func (m *defaultUserModel) DeleteSubscribe(ctx context.Context, token string, tx
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用 defer 确保删除后清理缓存
|
||||
defer func() {
|
||||
if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil {
|
||||
// 记录清理缓存错误
|
||||
}
|
||||
}()
|
||||
|
||||
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return m.execSubscribeMutation(ctx, []*Subscribe{data}, func(conn *gorm.DB) error {
|
||||
return conn.Where("token = ?", token).Delete(&Subscribe{}).Error
|
||||
})
|
||||
}, tx...)
|
||||
}
|
||||
|
||||
// InsertSubscribe insert Subscribe into the database.
|
||||
func (m *defaultUserModel) InsertSubscribe(ctx context.Context, data *Subscribe, tx ...*gorm.DB) error {
|
||||
// 使用 defer 确保插入后清理相关缓存
|
||||
defer func() {
|
||||
if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil {
|
||||
// 记录清理缓存错误
|
||||
}
|
||||
}()
|
||||
|
||||
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return m.execSubscribeMutation(ctx, []*Subscribe{data}, func(conn *gorm.DB) error {
|
||||
return conn.Create(data).Error
|
||||
})
|
||||
}, tx...)
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx ...*gorm.DB) error {
|
||||
@ -206,19 +189,9 @@ func (m *defaultUserModel) DeleteSubscribeById(ctx context.Context, id int64, tx
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用 defer 确保删除后清理缓存
|
||||
defer func() {
|
||||
if clearErr := m.ClearSubscribeCacheByModels(ctx, data); clearErr != nil {
|
||||
// 记录清理缓存错误
|
||||
}
|
||||
}()
|
||||
|
||||
return m.ExecNoCacheCtx(ctx, func(conn *gorm.DB) error {
|
||||
if len(tx) > 0 {
|
||||
conn = tx[0]
|
||||
}
|
||||
return m.execSubscribeMutation(ctx, []*Subscribe{data}, func(conn *gorm.DB) error {
|
||||
return conn.Where("id = ?", id).Delete(&Subscribe{}).Error
|
||||
})
|
||||
}, tx...)
|
||||
}
|
||||
|
||||
func (m *defaultUserModel) ClearSubscribeCache(ctx context.Context, data ...*Subscribe) error {
|
||||
|
||||
@ -558,7 +558,7 @@ type DeleteUserDeivceRequest struct {
|
||||
}
|
||||
|
||||
type DeleteUserSubscribeRequest struct {
|
||||
UserSubscribeId int64 `json:"user_subscribe_id,string"`
|
||||
UserSubscribeId int64 `json:"user_subscribe_id" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type DeviceAuthticateConfig struct {
|
||||
@ -1116,6 +1116,10 @@ type GetUserListRequest struct {
|
||||
SubscribeId *int64 `form:"subscribe_id,omitempty"`
|
||||
UserSubscribeId *int64 `form:"user_subscribe_id,omitempty"`
|
||||
ShortCode string `form:"short_code,omitempty"`
|
||||
FamilyJoined *bool `form:"family_joined,omitempty"`
|
||||
FamilyStatus string `form:"family_status,omitempty"`
|
||||
FamilyOwnerUserId *int64 `form:"family_owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
}
|
||||
|
||||
type GetUserListResponse struct {
|
||||
@ -1184,6 +1188,41 @@ type GetUserSubscribeResetTrafficLogsResponse struct {
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type GetFamilyListRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
Keyword string `form:"keyword,omitempty"`
|
||||
Status string `form:"status,omitempty"`
|
||||
OwnerUserId *int64 `form:"owner_user_id,omitempty"`
|
||||
FamilyId *int64 `form:"family_id,omitempty"`
|
||||
UserId *int64 `form:"user_id,omitempty"`
|
||||
}
|
||||
|
||||
type GetFamilyListResponse struct {
|
||||
List []FamilySummary `json:"list"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
type GetFamilyDetailRequest struct {
|
||||
Id int64 `form:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateFamilyMaxMembersRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
MaxMembers int64 `json:"max_members" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type RemoveFamilyMemberRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
UserId int64 `json:"user_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type DissolveFamilyRequest struct {
|
||||
FamilyId int64 `json:"family_id" validate:"required,gt=0"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type GetUserSubscribeTrafficLogsRequest struct {
|
||||
Page int `form:"page"`
|
||||
Size int `form:"size"`
|
||||
@ -1252,6 +1291,7 @@ type InviteConfig struct {
|
||||
ForcedInvite bool `json:"forced_invite"`
|
||||
ReferralPercentage int64 `json:"referral_percentage"`
|
||||
OnlyFirstPurchase bool `json:"only_first_purchase"`
|
||||
GiftDays int64 `json:"gift_days"`
|
||||
}
|
||||
|
||||
type KickOfflineRequest struct {
|
||||
@ -2674,8 +2714,18 @@ type User struct {
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
DeletedAt int64 `json:"deleted_at,omitempty"`
|
||||
IsDel bool `json:"is_del,omitempty"`
|
||||
Remark string `json:"remark,omitempty"`
|
||||
PurchasedPackage string `json:"purchased_package,omitempty"`
|
||||
LastLoginTime int64 `json:"last_login_time"`
|
||||
MemberStatus string `json:"member_status"`
|
||||
FamilyJoined bool `json:"family_joined,omitempty"`
|
||||
FamilyId int64 `json:"family_id,omitempty"`
|
||||
FamilyRole uint8 `json:"family_role,omitempty"`
|
||||
FamilyRoleName string `json:"family_role_name,omitempty"`
|
||||
FamilyOwnerUserId int64 `json:"family_owner_user_id,omitempty"`
|
||||
FamilyStatus string `json:"family_status,omitempty"`
|
||||
FamilyMemberCount int64 `json:"family_member_count,omitempty"`
|
||||
FamilyMaxMembers int64 `json:"family_max_members,omitempty"`
|
||||
}
|
||||
|
||||
type UserAffiliate struct {
|
||||
@ -2711,6 +2761,34 @@ type UserLoginLog struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type FamilySummary struct {
|
||||
FamilyId int64 `json:"family_id"`
|
||||
OwnerUserId int64 `json:"owner_user_id"`
|
||||
OwnerIdentifier string `json:"owner_identifier"`
|
||||
Status string `json:"status"`
|
||||
ActiveMemberCount int64 `json:"active_member_count"`
|
||||
MaxMembers int64 `json:"max_members"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
type FamilyMemberItem struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Identifier string `json:"identifier"`
|
||||
Role uint8 `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Status uint8 `json:"status"`
|
||||
StatusName string `json:"status_name"`
|
||||
JoinSource string `json:"join_source"`
|
||||
JoinedAt int64 `json:"joined_at"`
|
||||
LeftAt int64 `json:"left_at,omitempty"`
|
||||
}
|
||||
|
||||
type FamilyDetail struct {
|
||||
Summary FamilySummary `json:"summary"`
|
||||
Members []FamilyMemberItem `json:"members"`
|
||||
}
|
||||
|
||||
type UserLoginRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Email string `json:"email" validate:"required"`
|
||||
|
||||
@ -34,6 +34,9 @@ const (
|
||||
FamilyMemberLimitExceeded uint32 = 20014
|
||||
FamilyAlreadyBound uint32 = 20015
|
||||
FamilyCrossBindForbidden uint32 = 20016
|
||||
FamilyNotExist uint32 = 20017
|
||||
FamilyStatusInvalid uint32 = 20018
|
||||
FamilyOwnerOperationForbidden uint32 = 20019
|
||||
)
|
||||
|
||||
// Node error
|
||||
|
||||
@ -39,6 +39,9 @@ func init() {
|
||||
FamilyMemberLimitExceeded: "Family member limit exceeded",
|
||||
FamilyAlreadyBound: "Family already bound",
|
||||
FamilyCrossBindForbidden: "Cross-family binding is forbidden",
|
||||
FamilyNotExist: "Family does not exist",
|
||||
FamilyStatusInvalid: "Family status is invalid",
|
||||
FamilyOwnerOperationForbidden: "Owner operation is forbidden",
|
||||
|
||||
// Node error
|
||||
NodeExist: "Node already exists",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user