feat: 苹果支付uuid 及设备逻辑
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m3s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m3s
This commit is contained in:
parent
662ef6edb3
commit
26f6400e74
@ -665,7 +665,8 @@ type (
|
|||||||
FeeAmount int64 `json:"fee_amount"`
|
FeeAmount int64 `json:"fee_amount"`
|
||||||
}
|
}
|
||||||
PurchaseOrderResponse {
|
PurchaseOrderResponse {
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
|
AppAccountToken string `json:"app_account_token"`
|
||||||
}
|
}
|
||||||
RenewalOrderRequest {
|
RenewalOrderRequest {
|
||||||
UserSubscribeID int64 `json:"user_subscribe_id"`
|
UserSubscribeID int64 `json:"user_subscribe_id"`
|
||||||
@ -674,7 +675,8 @@ type (
|
|||||||
Coupon string `json:"coupon,omitempty"`
|
Coupon string `json:"coupon,omitempty"`
|
||||||
}
|
}
|
||||||
RenewalOrderResponse {
|
RenewalOrderResponse {
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
|
AppAccountToken string `json:"app_account_token"`
|
||||||
}
|
}
|
||||||
ResetTrafficOrderRequest {
|
ResetTrafficOrderRequest {
|
||||||
UserSubscribeID int64 `json:"user_subscribe_id"`
|
UserSubscribeID int64 `json:"user_subscribe_id"`
|
||||||
|
|||||||
@ -12,7 +12,10 @@ import (
|
|||||||
func UnbindDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
func UnbindDeviceHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
var req types.UnbindDeviceRequest
|
var req types.UnbindDeviceRequest
|
||||||
_ = c.ShouldBind(&req)
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
result.ParamErrorResult(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
validateErr := svcCtx.Validate(&req)
|
validateErr := svcCtx.Validate(&req)
|
||||||
if validateErr != nil {
|
if validateErr != nil {
|
||||||
result.ParamErrorResult(c, validateErr)
|
result.ParamErrorResult(c, validateErr)
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
@ -31,7 +33,7 @@ type PurchaseLogic struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CloseOrderTimeMinutes = 5
|
CloseOrderTimeMinutes = 15
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewPurchaseLogic creates a new purchase logic instance for subscription purchase operations.
|
// 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")
|
logger.Error("current user is not found in context")
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
if err = commonLogic.DenyIfFamilyMemberReadonly(l.ctx, l.svcCtx.DB, u.Id); err != nil {
|
// Resolve entitlement: member's subscription goes to owner
|
||||||
return nil, err
|
entitlement, entErr := commonLogic.ResolveEntitlementUser(l.ctx, l.svcCtx.DB, u.Id)
|
||||||
|
if entErr != nil {
|
||||||
|
return nil, entErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Quantity <= 0 {
|
if req.Quantity <= 0 {
|
||||||
@ -221,8 +225,9 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
}
|
}
|
||||||
// create order
|
// create order
|
||||||
orderInfo := &order.Order{
|
orderInfo := &order.Order{
|
||||||
UserId: u.Id,
|
UserId: u.Id,
|
||||||
ParentId: parentOrderID,
|
SubscriptionUserId: entitlement.EffectiveUserID,
|
||||||
|
ParentId: parentOrderID,
|
||||||
OrderNo: tool.GenerateTradeNo(),
|
OrderNo: tool.GenerateTradeNo(),
|
||||||
Type: orderType,
|
Type: orderType,
|
||||||
Quantity: req.Quantity,
|
Quantity: req.Quantity,
|
||||||
@ -237,8 +242,9 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P
|
|||||||
FeeAmount: feeAmount,
|
FeeAmount: feeAmount,
|
||||||
Status: 1,
|
Status: 1,
|
||||||
IsNew: isNew,
|
IsNew: isNew,
|
||||||
SubscribeId: targetSubscribeID,
|
SubscribeId: targetSubscribeID,
|
||||||
SubscribeToken: subscribeToken,
|
SubscribeToken: subscribeToken,
|
||||||
|
AppAccountToken: uuid.New().String(),
|
||||||
}
|
}
|
||||||
if isSingleModeRenewal {
|
if isSingleModeRenewal {
|
||||||
l.Infow("[Purchase] single mode purchase order created as renewal",
|
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{
|
return &types.PurchaseOrderResponse{
|
||||||
OrderNo: orderInfo.OrderNo,
|
OrderNo: orderInfo.OrderNo,
|
||||||
|
AppAccountToken: orderInfo.AppAccountToken,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
commonLogic "github.com/perfect-panel/server/internal/logic/common"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"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),
|
Method: canonicalOrderMethod(payment.Platform),
|
||||||
FeeAmount: feeAmount,
|
FeeAmount: feeAmount,
|
||||||
Status: 1,
|
Status: 1,
|
||||||
SubscribeId: userSubscribe.SubscribeId,
|
SubscribeId: userSubscribe.SubscribeId,
|
||||||
SubscribeToken: userSubscribe.Token,
|
SubscribeToken: userSubscribe.Token,
|
||||||
|
AppAccountToken: uuid.New().String(),
|
||||||
}
|
}
|
||||||
// Database transaction
|
// Database transaction
|
||||||
err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
|
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))
|
l.Infow("[Renewal] Enqueue task success", logger.Field("TaskID", taskInfo.ID))
|
||||||
}
|
}
|
||||||
return &types.RenewalOrderResponse{
|
return &types.RenewalOrderResponse{
|
||||||
OrderNo: orderInfo.OrderNo,
|
OrderNo: orderInfo.OrderNo,
|
||||||
|
AppAccountToken: orderInfo.AppAccountToken,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,23 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon
|
|||||||
|
|
||||||
l.clearAllSessions(currentUser.Id)
|
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
|
// 主动清 auth method 相关缓存(含 email/mobile 等 key),避免缓存未命中时无法生成正确 key
|
||||||
if len(authMethods) > 0 {
|
if len(authMethods) > 0 {
|
||||||
var authCacheKeys []string
|
var authCacheKeys []string
|
||||||
|
|||||||
@ -41,12 +41,29 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
|||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device")
|
return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device")
|
||||||
}
|
}
|
||||||
|
|
||||||
if device.UserId != userInfo.Id {
|
isSelf := (device.UserId == userInfo.Id)
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user")
|
|
||||||
|
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)
|
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 {
|
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)),
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import "time"
|
|||||||
type Order struct {
|
type Order struct {
|
||||||
Id int64 `gorm:"primaryKey"`
|
Id int64 `gorm:"primaryKey"`
|
||||||
ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"`
|
ParentId int64 `gorm:"type:bigint;default:null;comment:Parent Order Id"`
|
||||||
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id"`
|
UserId int64 `gorm:"type:bigint;not null;default:0;comment:User Id"`
|
||||||
|
SubscriptionUserId int64 `gorm:"type:bigint;not null;default:0;comment:Target user ID for subscription (0=same as UserId)"`
|
||||||
OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"`
|
OrderNo string `gorm:"type:varchar(255);not null;default:'';unique;comment:Order No"`
|
||||||
Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"`
|
Type uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Type: 1: Subscribe, 2: Renewal, 3: ResetTraffic, 4: Recharge"`
|
||||||
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"`
|
Quantity int64 `gorm:"type:bigint;not null;default:1;comment:Quantity"`
|
||||||
@ -22,8 +23,9 @@ type Order struct {
|
|||||||
TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"`
|
TradeNo string `gorm:"type:varchar(255);default:null;comment:Trade No"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"`
|
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Order Status: 1: Pending, 2: Paid, 3:Close, 4: Failed, 5:Finished;"`
|
||||||
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"`
|
SubscribeId int64 `gorm:"type:bigint;not null;default:0;comment:Subscribe Id"`
|
||||||
SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"`
|
SubscribeToken string `gorm:"type:varchar(255);default:null;comment:Renewal Subscribe Token"`
|
||||||
IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"`
|
AppAccountToken string `gorm:"type:varchar(36);default:null;comment:Apple IAP App Account Token (UUID)"`
|
||||||
|
IsNew bool `gorm:"type:tinyint(1);not null;default:0;comment:Is New Order"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Create Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,13 +13,13 @@ const (
|
|||||||
FamilyMemberActive uint8 = 1
|
FamilyMemberActive uint8 = 1
|
||||||
FamilyMemberLeft uint8 = 2
|
FamilyMemberLeft uint8 = 2
|
||||||
FamilyMemberRemoved uint8 = 3
|
FamilyMemberRemoved uint8 = 3
|
||||||
DefaultFamilyMaxSize int64 = 5
|
DefaultFamilyMaxSize int64 = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserFamily struct {
|
type UserFamily struct {
|
||||||
Id int64 `gorm:"primaryKey"`
|
Id int64 `gorm:"primaryKey"`
|
||||||
OwnerUserId int64 `gorm:"uniqueIndex:uniq_owner_user_id;not null;comment:Owner User ID"`
|
OwnerUserId int64 `gorm:"uniqueIndex:uniq_owner_user_id;not null;comment:Owner User ID"`
|
||||||
MaxMembers int64 `gorm:"not null;default:5;comment:Max members in family"`
|
MaxMembers int64 `gorm:"not null;default:2;comment:Max members in family"`
|
||||||
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status: 1=active, 0=disabled"`
|
Status uint8 `gorm:"type:tinyint(1);not null;default:1;comment:Status: 1=active, 0=disabled"`
|
||||||
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
|
||||||
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
UpdatedAt time.Time `gorm:"comment:Update Time"`
|
||||||
|
|||||||
@ -1846,7 +1846,8 @@ type PurchaseOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseOrderResponse struct {
|
type PurchaseOrderResponse struct {
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
|
AppAccountToken string `json:"app_account_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type QueryAnnouncementRequest struct {
|
type QueryAnnouncementRequest struct {
|
||||||
@ -2129,7 +2130,8 @@ type RenewalOrderRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RenewalOrderResponse struct {
|
type RenewalOrderResponse struct {
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
|
AppAccountToken string `json:"app_account_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResetAllSubscribeTokenResponse struct {
|
type ResetAllSubscribeTokenResponse struct {
|
||||||
|
|||||||
@ -471,8 +471,15 @@ func (l *ActivateOrderLogic) getSubscribeInfo(ctx context.Context, subscribeId i
|
|||||||
// createUserSubscription creates a new user subscription record based on order and subscription plan details
|
// createUserSubscription creates a new user subscription record based on order and subscription plan details
|
||||||
func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) {
|
func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderInfo *order.Order, sub *subscribe.Subscribe) (*user.Subscribe, error) {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
|
// Determine subscription owner: use SubscriptionUserId if set, otherwise fall back to UserId
|
||||||
|
subscriptionUserId := orderInfo.UserId
|
||||||
|
if orderInfo.SubscriptionUserId > 0 {
|
||||||
|
subscriptionUserId = orderInfo.SubscriptionUserId
|
||||||
|
}
|
||||||
|
|
||||||
userSub := &user.Subscribe{
|
userSub := &user.Subscribe{
|
||||||
UserId: orderInfo.UserId,
|
UserId: subscriptionUserId,
|
||||||
OrderId: orderInfo.Id,
|
OrderId: orderInfo.Id,
|
||||||
SubscribeId: orderInfo.SubscribeId,
|
SubscribeId: orderInfo.SubscribeId,
|
||||||
StartTime: now,
|
StartTime: now,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user