This commit is contained in:
@@ -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"
|
||||
@@ -57,13 +58,17 @@ type LoginLogFilterParams struct {
|
||||
}
|
||||
|
||||
type UserFilterParams struct {
|
||||
Search string
|
||||
UserId *int64
|
||||
SubscribeId *int64
|
||||
UserSubscribeId *int64
|
||||
ShortCode string
|
||||
Order string // Order by id, e.g., "desc"
|
||||
Unscoped bool // Whether to include soft-deleted records
|
||||
Search string
|
||||
UserId *int64
|
||||
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
|
||||
}
|
||||
|
||||
type customUserLogicModel interface {
|
||||
@@ -117,8 +122,9 @@ type customUserLogicModel interface {
|
||||
}
|
||||
|
||||
type UserStatusInfo struct {
|
||||
MemberStatus string
|
||||
LastTrafficAt *time.Time
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user