feat: 苹果支付uuid 及设备逻辑
Build docker and publish / build (20.15.1) (push) Successful in 8m3s

This commit is contained in:
2026-03-10 19:53:19 -07:00
parent 662ef6edb3
commit 26f6400e74
10 changed files with 142 additions and 25 deletions
+15 -8
View File
@@ -6,6 +6,8 @@ import (
"math"
"time"
"github.com/google/uuid"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/pkg/constant"
@@ -31,7 +33,7 @@ type PurchaseLogic struct {
}
const (
CloseOrderTimeMinutes = 5
CloseOrderTimeMinutes = 15
)
// NewPurchaseLogic creates a new purchase logic instance for subscription purchase operations.
@@ -54,8 +56,10 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
if err = commonLogic.DenyIfFamilyMemberReadonly(l.ctx, l.svcCtx.DB, u.Id); err != nil {
return nil, err
// Resolve entitlement: member's subscription goes to owner
entitlement, entErr := commonLogic.ResolveEntitlementUser(l.ctx, l.svcCtx.DB, u.Id)
if entErr != nil {
return nil, entErr
}
if req.Quantity <= 0 {
@@ -221,8 +225,9 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
}
// create order
orderInfo := &order.Order{
UserId: u.Id,
ParentId: parentOrderID,
UserId: u.Id,
SubscriptionUserId: entitlement.EffectiveUserID,
ParentId: parentOrderID,
OrderNo: tool.GenerateTradeNo(),
Type: orderType,
Quantity: req.Quantity,
@@ -237,8 +242,9 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
FeeAmount: feeAmount,
Status: 1,
IsNew: isNew,
SubscribeId: targetSubscribeID,
SubscribeToken: subscribeToken,
SubscribeId: targetSubscribeID,
SubscribeToken: subscribeToken,
AppAccountToken: uuid.New().String(),
}
if isSingleModeRenewal {
l.Infow("[Purchase] single mode purchase order created as renewal",
@@ -368,6 +374,7 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
}
return &types.PurchaseOrderResponse{
OrderNo: orderInfo.OrderNo,
OrderNo: orderInfo.OrderNo,
AppAccountToken: orderInfo.AppAccountToken,
}, nil
}
+7 -3
View File
@@ -6,6 +6,8 @@ import (
"math"
"time"
"github.com/google/uuid"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/pkg/constant"
@@ -182,8 +184,9 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
Method: canonicalOrderMethod(payment.Platform),
FeeAmount: feeAmount,
Status: 1,
SubscribeId: userSubscribe.SubscribeId,
SubscribeToken: userSubscribe.Token,
SubscribeId: userSubscribe.SubscribeId,
SubscribeToken: userSubscribe.Token,
AppAccountToken: uuid.New().String(),
}
// Database transaction
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
@@ -239,6 +242,7 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
l.Infow("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
}
return &types.RenewalOrderResponse{
OrderNo: orderInfo.OrderNo,
OrderNo: orderInfo.OrderNo,
AppAccountToken: orderInfo.AppAccountToken,
}, nil
}
@@ -78,6 +78,23 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
l.clearAllSessions(currentUser.Id)
// Kick all affected family member devices + clear their sessions
for _, memberUserID := range affectedUserIDs {
if memberUserID == currentUser.Id {
continue
}
var memberDevices []user.Device
l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Device{}).
Where("user_id = ?", memberUserID).
Find(&memberDevices)
for _, d := range memberDevices {
l.svcCtx.DeviceManager.KickDevice(d.UserId, d.Identifier)
}
l.clearAllSessions(memberUserID)
}
// 主动清 auth method 相关缓存(含 email/mobile 等 key),避免缓存未命中时无法生成正确 key
if len(authMethods) > 0 {
var authCacheKeys []string
@@ -41,12 +41,29 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device")
}
if device.UserId != userInfo.Id {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user")
isSelf := (device.UserId == userInfo.Id)
if !isSelf {
// Not own device — check if in the same family
if !l.isInSameFamily(userInfo.Id, device.UserId) {
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "not in same family")
}
}
targetUser, err := l.svcCtx.UserModel.FindOne(l.ctx, device.UserId)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find target user")
}
// If kicking another user (not self), check if subscription transfer is needed
if !isSelf {
if err := l.transferSubscriptionsIfNeeded(userInfo, targetUser); err != nil {
return err
}
}
currentSessionID, _ := l.ctx.Value(constant.CtxKeySessionID).(string)
return l.logoutUnbind(userInfo, device, currentSessionID)
return l.logoutUnbind(targetUser, device, currentSessionID)
}
func (l *UnbindDeviceLogic) logoutUnbind(userInfo *user.User, device *user.Device, currentSessionID string) error {
@@ -177,3 +194,59 @@ func (l *UnbindDeviceLogic) clearAllSessions(userId int64) {
logger.Field("count", len(sessions)),
)
}
// isInSameFamily checks whether two users belong to the same active family
func (l *UnbindDeviceLogic) isInSameFamily(userID1, userID2 int64) bool {
var relation1 struct{ FamilyId int64 }
err := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.UserFamilyMember{}).
Select("family_id").
Where("user_id = ? AND status = ?", userID1, user.FamilyMemberActive).
First(&relation1).Error
if err != nil || relation1.FamilyId == 0 {
return false
}
var count int64
l.svcCtx.DB.WithContext(l.ctx).
Model(&user.UserFamilyMember{}).
Where("family_id = ? AND user_id = ? AND status = ?",
relation1.FamilyId, userID2, user.FamilyMemberActive).
Count(&count)
return count > 0
}
// transferSubscriptionsIfNeeded transfers subscriptions from the kicked user to the kicker
func (l *UnbindDeviceLogic) transferSubscriptionsIfNeeded(kicker *user.User, kicked *user.User) error {
// Query kicked user's subscriptions
var subscribes []user.Subscribe
if err := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Subscribe{}).
Where("user_id = ?", kicked.Id).
Find(&subscribes).Error; err != nil {
return nil // query error, skip transfer
}
if len(subscribes) == 0 {
return nil // no subscriptions to transfer
}
// Transfer subscriptions: UPDATE user_subscribe SET user_id = kicker WHERE user_id = kicked
if err := l.svcCtx.DB.WithContext(l.ctx).
Model(&user.Subscribe{}).
Where("user_id = ?", kicked.Id).
Update("user_id", kicker.Id).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "transfer subscriptions failed")
}
// Clear subscription caches for both users
for _, sub := range subscribes {
_ = l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, &sub)
}
l.Infow("subscriptions transferred",
logger.Field("from_user_id", kicked.Id),
logger.Field("to_user_id", kicker.Id),
logger.Field("count", len(subscribes)),
)
return nil
}