Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d748a7e75d | |||
| cf70838142 | |||
| 280437be91 | |||
| 59b7056a20 | |||
| 769622f087 |
@@ -21,7 +21,7 @@ env:
|
|||||||
SSH_PASSWORD: ${{ github.ref_name == 'main' && vars.SSH_PASSWORD || vars.DEV_SSH_PASSWORD }}
|
SSH_PASSWORD: ${{ github.ref_name == 'main' && vars.SSH_PASSWORD || vars.DEV_SSH_PASSWORD }}
|
||||||
# TG通知
|
# TG通知
|
||||||
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
|
||||||
TG_CHAT_ID: "-4940243803"
|
TG_CHAT_ID: "-49402438031"
|
||||||
# Go构建变量
|
# Go构建变量
|
||||||
SERVICE: vpn
|
SERVICE: vpn
|
||||||
SERVICE_STYLE: vpn
|
SERVICE_STYLE: vpn
|
||||||
|
|||||||
@@ -699,6 +699,85 @@ func TestPurchase_NewUserOnly_BindEmailScopeSharesHistory(t *testing.T) {
|
|||||||
assert.Equal(t, int64(0), newOrder.Discount)
|
assert.Equal(t, int64(0), newOrder.Discount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureRenewalSubscribeColumns(t *testing.T, db *gorm.DB) {
|
||||||
|
t.Helper()
|
||||||
|
for _, sql := range []string{
|
||||||
|
`ALTER TABLE "user_subscribe" ADD COLUMN node_group_id INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE "user_subscribe" ADD COLUMN group_locked TINYINT NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE "user_subscribe" ADD COLUMN expired_download INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
`ALTER TABLE "user_subscribe" ADD COLUMN expired_upload INTEGER NOT NULL DEFAULT 0`,
|
||||||
|
} {
|
||||||
|
require.NoError(t, db.Exec(sql).Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertRenewalUserSubscribe(t *testing.T, db *gorm.DB, id, userID, orderID, subscribeID int64, token, uuid string, start, expire time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
require.NoError(t, db.Exec(`INSERT INTO "user_subscribe"
|
||||||
|
(id, user_id, order_id, subscribe_id, start_time, expire_time, traffic, download, upload, token, uuid, status, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 1, ?, ?)`,
|
||||||
|
id,
|
||||||
|
userID,
|
||||||
|
orderID,
|
||||||
|
subscribeID,
|
||||||
|
start.UTC().Format("2006-01-02 15:04:05"),
|
||||||
|
expire.UTC().Format("2006-01-02 15:04:05"),
|
||||||
|
token,
|
||||||
|
uuid,
|
||||||
|
start.UTC().Format("2006-01-02 15:04:05"),
|
||||||
|
time.Now().UTC().Format("2006-01-02 15:04:05"),
|
||||||
|
).Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenewalMemberRequestRedirectsToOwnerSubscribe(t *testing.T) {
|
||||||
|
db := setupNewUserOnlyDB(t)
|
||||||
|
ensureRenewalSubscribeColumns(t, db)
|
||||||
|
rds, mr := setupNewUserOnlyRedis(t)
|
||||||
|
svcCtx := buildNewUserOnlySvcCtx(db, rds, mr)
|
||||||
|
|
||||||
|
const (
|
||||||
|
planID = int64(1)
|
||||||
|
paymentID = int64(2)
|
||||||
|
ownerUserID = int64(29650)
|
||||||
|
memberID = int64(20003)
|
||||||
|
familyID = int64(88001)
|
||||||
|
memberSubID = int64(10013)
|
||||||
|
ownerSubID = int64(14074)
|
||||||
|
)
|
||||||
|
|
||||||
|
insertTestSubscribe(t, db, planID, false)
|
||||||
|
insertTestPayment(t, db, paymentID)
|
||||||
|
member := insertTestUser(t, db, memberID, time.Now().Add(-24*time.Hour))
|
||||||
|
insertTestUser(t, db, ownerUserID, time.Now().Add(-24*time.Hour))
|
||||||
|
insertTestFamily(t, db, familyID, ownerUserID)
|
||||||
|
insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init")
|
||||||
|
insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite")
|
||||||
|
|
||||||
|
insertRenewalUserSubscribe(t, db, memberSubID, memberID, 7448, planID, "member-token-10013", "member-uuid-10013",
|
||||||
|
time.Date(2026, 4, 23, 19, 6, 40, 0, time.UTC),
|
||||||
|
time.Date(2026, 4, 30, 19, 6, 40, 0, time.UTC),
|
||||||
|
)
|
||||||
|
insertRenewalUserSubscribe(t, db, ownerSubID, ownerUserID, 9999, planID, "owner-token-14074", "owner-uuid-14074",
|
||||||
|
time.Date(2026, 4, 30, 13, 39, 33, 0, time.UTC),
|
||||||
|
time.Date(2026, 5, 30, 16, 0, 0, 0, time.UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{
|
||||||
|
UserSubscribeID: memberSubID,
|
||||||
|
Payment: paymentID,
|
||||||
|
Quantity: 30,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, resp)
|
||||||
|
|
||||||
|
var created modelOrder.Order
|
||||||
|
require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error)
|
||||||
|
assert.Equal(t, memberID, created.UserId)
|
||||||
|
assert.Equal(t, ownerUserID, created.SubscriptionUserId)
|
||||||
|
assert.Equal(t, int64(9999), created.ParentId)
|
||||||
|
assert.Equal(t, "owner-token-14074", created.SubscribeToken)
|
||||||
|
}
|
||||||
|
|
||||||
func TestPreCreateOrder_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
|
func TestPreCreateOrder_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) {
|
||||||
db := setupNewUserOnlyDB(t)
|
db := setupNewUserOnlyDB(t)
|
||||||
rds, mr := setupNewUserOnlyRedis(t)
|
rds, mr := setupNewUserOnlyRedis(t)
|
||||||
|
|||||||
@@ -41,6 +41,67 @@ func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *RenewalLogic) resolveRenewalTargetSubscribe(requested *user.SubscribeDetails, entitlement *commonLogic.EntitlementContext, currentUserID int64) (*user.SubscribeDetails, error) {
|
||||||
|
if requested == nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "requested user subscribe is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveUserID := currentUserID
|
||||||
|
if entitlement != nil && entitlement.EffectiveUserID > 0 {
|
||||||
|
effectiveUserID = entitlement.EffectiveUserID
|
||||||
|
}
|
||||||
|
|
||||||
|
if effectiveUserID == currentUserID {
|
||||||
|
if requested.UserId != currentUserID {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "user subscribe does not belong to current user")
|
||||||
|
}
|
||||||
|
return requested, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if requested.UserId != currentUserID && requested.UserId != effectiveUserID {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "user subscribe does not belong to current family entitlement")
|
||||||
|
}
|
||||||
|
if requested.UserId == effectiveUserID {
|
||||||
|
return requested, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerSubscribe, err := l.findOwnerRenewalSubscribe(effectiveUserID, requested.SubscribeId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "renewal_target_resolved",
|
||||||
|
"[SubscriptionFlow] renewal target redirected to family owner subscription",
|
||||||
|
logger.Field("user_id", currentUserID),
|
||||||
|
logger.Field("effective_user_id", effectiveUserID),
|
||||||
|
logger.Field("requested_user_subscribe_id", requested.Id),
|
||||||
|
logger.Field("requested_subscribe_owner_user_id", requested.UserId),
|
||||||
|
logger.Field("resolved_user_subscribe_id", ownerSubscribe.Id),
|
||||||
|
logger.Field("resolved_subscribe_owner_user_id", ownerSubscribe.UserId),
|
||||||
|
logger.Field("subscribe_id", requested.SubscribeId),
|
||||||
|
)
|
||||||
|
return ownerSubscribe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *RenewalLogic) findOwnerRenewalSubscribe(ownerUserID, subscribeID int64) (*user.SubscribeDetails, error) {
|
||||||
|
var target user.SubscribeDetails
|
||||||
|
err := l.svcCtx.DB.WithContext(l.ctx).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Preload("Subscribe").
|
||||||
|
Where("user_id = ? AND subscribe_id = ? AND token != ''", ownerUserID, subscribeID).
|
||||||
|
Where("status IN ?", []int64{0, 1, 2, 3}).
|
||||||
|
Order("expire_time DESC").
|
||||||
|
Order("updated_at DESC").
|
||||||
|
Order("id DESC").
|
||||||
|
First(&target).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "owner subscribe not found for renewal")
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find owner subscribe error: %v", err.Error())
|
||||||
|
}
|
||||||
|
return &target, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Renewal processes subscription renewal orders including discount calculation,
|
// Renewal processes subscription renewal orders including discount calculation,
|
||||||
// coupon validation, gift amount deduction, fee calculation, and order creation
|
// coupon validation, gift amount deduction, fee calculation, and order creation
|
||||||
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) {
|
func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) {
|
||||||
@@ -77,11 +138,15 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
|||||||
}
|
}
|
||||||
|
|
||||||
orderNo := tool.GenerateTradeNo()
|
orderNo := tool.GenerateTradeNo()
|
||||||
// find user subscribe
|
// find requested user subscribe, then resolve it to the real entitlement owner.
|
||||||
userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
|
requestedSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
userSubscribe, err := l.resolveRenewalTargetSubscribe(requestedSubscribe, entitlement, u.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
// find subscription
|
// find subscription
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId)
|
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -251,7 +316,9 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene
|
|||||||
"[SubscriptionFlow] renewal order persisted",
|
"[SubscriptionFlow] renewal order persisted",
|
||||||
append(commonLogic.OrderTraceFields(&orderInfo),
|
append(commonLogic.OrderTraceFields(&orderInfo),
|
||||||
logger.Field("requested_user_subscribe_id", req.UserSubscribeID),
|
logger.Field("requested_user_subscribe_id", req.UserSubscribeID),
|
||||||
|
logger.Field("requested_subscribe_owner_user_id", requestedSubscribe.UserId),
|
||||||
logger.Field("resolved_user_subscribe_id", userSubscribe.Id),
|
logger.Field("resolved_user_subscribe_id", userSubscribe.Id),
|
||||||
|
logger.Field("resolved_subscribe_owner_user_id", userSubscribe.UserId),
|
||||||
)...,
|
)...,
|
||||||
)
|
)
|
||||||
// Deferred task
|
// Deferred task
|
||||||
|
|||||||
@@ -135,16 +135,18 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
logger.Field("email", req.Email),
|
logger.Field("email", req.Email),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Join family: email user as owner, device user as member
|
// Join family: email user as owner, device user as member.
|
||||||
|
// For a newly-created email owner, preserve the device user's paid entitlement by moving
|
||||||
|
// its subscriptions to the email owner and issuing owner-side subscription tokens.
|
||||||
if err = familyHelper.validateJoinFamily(emailUser.Id, u.Id); err != nil {
|
if err = familyHelper.validateJoinFamily(emailUser.Id, u.Id); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
joinResult, err := familyHelper.joinFamily(emailUser.Id, u.Id, "bind_email_with_verification")
|
joinResult, err := familyHelper.joinFamilyAndMoveSubscribesToOwner(emailUser.Id, u.Id, "bind_email_with_verification")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined",
|
commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined",
|
||||||
"[SubscriptionFlow] device user joined email owner family",
|
"[SubscriptionFlow] device user joined email owner family and subscriptions moved to owner with reset tokens",
|
||||||
logger.Field("device_user_id", u.Id),
|
logger.Field("device_user_id", u.Id),
|
||||||
logger.Field("owner_user_id", emailUser.Id),
|
logger.Field("owner_user_id", emailUser.Id),
|
||||||
logger.Field("family_id", joinResult.FamilyId),
|
logger.Field("family_id", joinResult.FamilyId),
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package user
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/pkg/uuidx"
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -22,6 +25,13 @@ type familyBindingHelper struct {
|
|||||||
svcCtx *svc.ServiceContext
|
svcCtx *svc.ServiceContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type familySubscribeTransferMode uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
familySubscribeDiscard familySubscribeTransferMode = iota
|
||||||
|
familySubscribeMoveToOwner
|
||||||
|
)
|
||||||
|
|
||||||
func newFamilyBindingHelper(ctx context.Context, svcCtx *svc.ServiceContext) *familyBindingHelper {
|
func newFamilyBindingHelper(ctx context.Context, svcCtx *svc.ServiceContext) *familyBindingHelper {
|
||||||
return &familyBindingHelper{
|
return &familyBindingHelper{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -111,6 +121,14 @@ func (h *familyBindingHelper) validateJoinFamily(ownerUserId, memberUserId int64
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source string) (*familyJoinResult, error) {
|
func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source string) (*familyJoinResult, error) {
|
||||||
|
return h.joinFamilyWithSubscribeMode(ownerUserId, memberUserId, source, familySubscribeDiscard)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *familyBindingHelper) joinFamilyAndMoveSubscribesToOwner(ownerUserId, memberUserId int64, source string) (*familyJoinResult, error) {
|
||||||
|
return h.joinFamilyWithSubscribeMode(ownerUserId, memberUserId, source, familySubscribeMoveToOwner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *familyBindingHelper) joinFamilyWithSubscribeMode(ownerUserId, memberUserId int64, source string, subscribeMode familySubscribeTransferMode) (*familyJoinResult, error) {
|
||||||
if ownerUserId == memberUserId {
|
if ownerUserId == memberUserId {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already bound to this family")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.FamilyAlreadyBound), "user already bound to this family")
|
||||||
}
|
}
|
||||||
@@ -118,7 +136,7 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source
|
|||||||
result := &familyJoinResult{
|
result := &familyJoinResult{
|
||||||
OwnerUserId: ownerUserId,
|
OwnerUserId: ownerUserId,
|
||||||
}
|
}
|
||||||
removedSubscribes := make([]user.Subscribe, 0)
|
affectedSubscribes := make([]user.Subscribe, 0)
|
||||||
|
|
||||||
err := h.svcCtx.DB.WithContext(h.ctx).Transaction(func(tx *gorm.DB) error {
|
err := h.svcCtx.DB.WithContext(h.ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
ownerFamily, err := h.getOrCreateOwnerFamily(tx, ownerUserId)
|
ownerFamily, err := h.getOrCreateOwnerFamily(tx, ownerUserId)
|
||||||
@@ -182,10 +200,18 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removedSubscribes, err = transferMemberSubscribesToOwner(tx, memberUserId, ownerUserId)
|
switch subscribeMode {
|
||||||
|
case familySubscribeMoveToOwner:
|
||||||
|
affectedSubscribes, err = moveMemberSubscribesToOwner(tx, memberUserId, ownerUserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
affectedSubscribes, err = transferMemberSubscribesToOwner(tx, memberUserId, ownerUserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -193,12 +219,56 @@ func (h *familyBindingHelper) joinFamily(ownerUserId, memberUserId int64, source
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = h.clearRemovedMemberSubscribeCache(removedSubscribes); err != nil {
|
if subscribeMode == familySubscribeMoveToOwner {
|
||||||
|
if err = h.clearMovedMemberSubscribeCache(affectedSubscribes, ownerUserId); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if err = h.clearRemovedMemberSubscribeCache(affectedSubscribes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func moveMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int64) ([]user.Subscribe, error) {
|
||||||
|
var subscribes []user.Subscribe
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Where("user_id = ?", memberUserId).
|
||||||
|
Find(&subscribes).Error; err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query member subscribe list failed")
|
||||||
|
}
|
||||||
|
if len(subscribes) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, sub := range subscribes {
|
||||||
|
newToken := uuidx.SubscribeToken(fmt.Sprintf("familyMove:%d:%d:%s", ownerUserId, sub.Id, uuidx.NewUUID().String()))
|
||||||
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
|
Where("id = ? AND user_id = ?", sub.Id, memberUserId).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"user_id": ownerUserId,
|
||||||
|
"token": newToken,
|
||||||
|
"uuid": uuidx.NewUUID().String(),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "move member subscribe to owner failed")
|
||||||
|
}
|
||||||
|
if sub.OrderId > 0 {
|
||||||
|
if err := tx.Model(&modelOrder.Order{}).
|
||||||
|
Where("id = ? AND subscription_user_id = ?", sub.OrderId, memberUserId).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"subscription_user_id": ownerUserId,
|
||||||
|
"subscribe_token": newToken,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "move member subscribe order to owner failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subscribes, nil
|
||||||
|
}
|
||||||
|
|
||||||
func transferMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int64) ([]user.Subscribe, error) {
|
func transferMemberSubscribesToOwner(tx *gorm.DB, memberUserId, ownerUserId int64) ([]user.Subscribe, error) {
|
||||||
var subscribes []user.Subscribe
|
var subscribes []user.Subscribe
|
||||||
if err := tx.Model(&user.Subscribe{}).
|
if err := tx.Model(&user.Subscribe{}).
|
||||||
@@ -238,6 +308,37 @@ func (h *familyBindingHelper) clearRemovedMemberSubscribeCache(removedSubscribes
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *familyBindingHelper) clearMovedMemberSubscribeCache(movedSubscribes []user.Subscribe, ownerUserId int64) error {
|
||||||
|
if len(movedSubscribes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheModels := make([]*user.Subscribe, 0, len(movedSubscribes)*2)
|
||||||
|
ownerCopies := make([]user.Subscribe, len(movedSubscribes))
|
||||||
|
for i := range movedSubscribes {
|
||||||
|
cacheModels = append(cacheModels, &movedSubscribes[i])
|
||||||
|
ownerCopies[i] = movedSubscribes[i]
|
||||||
|
ownerCopies[i].UserId = ownerUserId
|
||||||
|
cacheModels = append(cacheModels, &ownerCopies[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.svcCtx.UserModel.ClearSubscribeCache(h.ctx, cacheModels...); err != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear moved subscribe cache failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, subscribeIDSet := buildRemovedSubscribeCacheMeta(movedSubscribes)
|
||||||
|
for subscribeID := range subscribeIDSet {
|
||||||
|
if err := h.svcCtx.SubscribeModel.ClearCache(h.ctx, subscribeID); err != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear subscribe cache failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := h.svcCtx.NodeModel.ClearServerAllCache(h.ctx); err != nil {
|
||||||
|
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "clear node cache failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildRemovedSubscribeCacheMeta(removedSubscribes []user.Subscribe) ([]*user.Subscribe, map[int64]struct{}) {
|
func buildRemovedSubscribeCacheMeta(removedSubscribes []user.Subscribe) ([]*user.Subscribe, map[int64]struct{}) {
|
||||||
subscribeModels := make([]*user.Subscribe, 0, len(removedSubscribes))
|
subscribeModels := make([]*user.Subscribe, 0, len(removedSubscribes))
|
||||||
subscribeIDSet := make(map[int64]struct{}, len(removedSubscribes))
|
subscribeIDSet := make(map[int64]struct{}, len(removedSubscribes))
|
||||||
|
|||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
modelOrder "github.com/perfect-panel/server/internal/model/order"
|
||||||
|
modelUser "github.com/perfect-panel/server/internal/model/user"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newFamilyBindingTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open sqlite db: %v", err)
|
||||||
|
}
|
||||||
|
if err = db.Exec(`
|
||||||
|
CREATE TABLE user_subscribe (
|
||||||
|
id integer primary key,
|
||||||
|
user_id integer not null,
|
||||||
|
order_id integer not null,
|
||||||
|
subscribe_id integer not null,
|
||||||
|
start_time datetime,
|
||||||
|
expire_time datetime,
|
||||||
|
token text,
|
||||||
|
uuid text,
|
||||||
|
status integer,
|
||||||
|
created_at datetime,
|
||||||
|
updated_at datetime
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("create user_subscribe schema: %v", err)
|
||||||
|
}
|
||||||
|
if err = db.Exec(`
|
||||||
|
CREATE TABLE "order" (
|
||||||
|
id integer primary key,
|
||||||
|
user_id integer not null,
|
||||||
|
subscription_user_id integer not null,
|
||||||
|
order_no text,
|
||||||
|
subscribe_token text,
|
||||||
|
status integer,
|
||||||
|
subscribe_id integer,
|
||||||
|
created_at datetime,
|
||||||
|
updated_at datetime
|
||||||
|
)`).Error; err != nil {
|
||||||
|
t.Fatalf("create order schema: %v", err)
|
||||||
|
}
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertFamilyBindingTestOrder(t *testing.T, db *gorm.DB, order modelOrder.Order) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO "order" (
|
||||||
|
id, user_id, subscription_user_id, order_no, subscribe_token, status, subscribe_id, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
order.Id,
|
||||||
|
order.UserId,
|
||||||
|
order.SubscriptionUserId,
|
||||||
|
order.OrderNo,
|
||||||
|
order.SubscribeToken,
|
||||||
|
order.Status,
|
||||||
|
order.SubscribeId,
|
||||||
|
order.CreatedAt,
|
||||||
|
order.UpdatedAt,
|
||||||
|
).Error; err != nil {
|
||||||
|
t.Fatalf("insert order %d: %v", order.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertFamilyBindingTestSubscribe(t *testing.T, db *gorm.DB, sub modelUser.Subscribe) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if err := db.Exec(`
|
||||||
|
INSERT INTO user_subscribe (
|
||||||
|
id, user_id, order_id, subscribe_id, start_time, expire_time, token, uuid, status, created_at, updated_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
sub.Id,
|
||||||
|
sub.UserId,
|
||||||
|
sub.OrderId,
|
||||||
|
sub.SubscribeId,
|
||||||
|
sub.StartTime,
|
||||||
|
sub.ExpireTime,
|
||||||
|
sub.Token,
|
||||||
|
sub.UUID,
|
||||||
|
sub.Status,
|
||||||
|
sub.CreatedAt,
|
||||||
|
sub.UpdatedAt,
|
||||||
|
).Error; err != nil {
|
||||||
|
t.Fatalf("insert subscribe %d: %v", sub.Id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMoveMemberSubscribesToOwnerMovesSubscribeAndCurrentOrder(t *testing.T) {
|
||||||
|
db := newFamilyBindingTestDB(t)
|
||||||
|
memberUserID := int64(1001)
|
||||||
|
ownerUserID := int64(2001)
|
||||||
|
now := time.Now().Add(-time.Hour)
|
||||||
|
expireAt := now.Add(30 * 24 * time.Hour)
|
||||||
|
|
||||||
|
currentOrder := modelOrder.Order{
|
||||||
|
Id: 9001,
|
||||||
|
UserId: memberUserID,
|
||||||
|
SubscriptionUserId: memberUserID,
|
||||||
|
OrderNo: "order-current",
|
||||||
|
SubscribeToken: "token-current",
|
||||||
|
Status: 5,
|
||||||
|
SubscribeId: 3001,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
unlinkedOrder := modelOrder.Order{
|
||||||
|
Id: 9002,
|
||||||
|
UserId: memberUserID,
|
||||||
|
SubscriptionUserId: memberUserID,
|
||||||
|
OrderNo: "order-unlinked",
|
||||||
|
SubscribeToken: "token-unlinked",
|
||||||
|
Status: 5,
|
||||||
|
SubscribeId: 3001,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
sub := modelUser.Subscribe{
|
||||||
|
Id: 7001,
|
||||||
|
UserId: memberUserID,
|
||||||
|
OrderId: currentOrder.Id,
|
||||||
|
SubscribeId: currentOrder.SubscribeId,
|
||||||
|
StartTime: now,
|
||||||
|
ExpireTime: expireAt,
|
||||||
|
Token: "token-current",
|
||||||
|
UUID: "uuid-current",
|
||||||
|
Status: 1,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
insertFamilyBindingTestOrder(t, db, currentOrder)
|
||||||
|
insertFamilyBindingTestOrder(t, db, unlinkedOrder)
|
||||||
|
insertFamilyBindingTestSubscribe(t, db, sub)
|
||||||
|
|
||||||
|
var moved []modelUser.Subscribe
|
||||||
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var err error
|
||||||
|
moved, err = moveMemberSubscribesToOwner(tx, memberUserID, ownerUserID)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("move member subscribes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(moved) != 1 {
|
||||||
|
t.Fatalf("moved subscribes length = %d, want 1", len(moved))
|
||||||
|
}
|
||||||
|
if moved[0].UserId != memberUserID {
|
||||||
|
t.Fatalf("moved cache copy user_id = %d, want original member %d", moved[0].UserId, memberUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotSub modelUser.Subscribe
|
||||||
|
if err := db.First(&gotSub, "id = ?", sub.Id).Error; err != nil {
|
||||||
|
t.Fatalf("query moved subscribe: %v", err)
|
||||||
|
}
|
||||||
|
if gotSub.UserId != ownerUserID {
|
||||||
|
t.Fatalf("subscribe user_id = %d, want owner %d", gotSub.UserId, ownerUserID)
|
||||||
|
}
|
||||||
|
if gotSub.Token == "" || gotSub.Token == sub.Token {
|
||||||
|
t.Fatalf("subscribe token = %q, want regenerated from old token %q", gotSub.Token, sub.Token)
|
||||||
|
}
|
||||||
|
if gotSub.UUID == "" || gotSub.UUID == sub.UUID {
|
||||||
|
t.Fatalf("subscribe uuid = %q, want regenerated from old uuid %q", gotSub.UUID, sub.UUID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotOrder modelOrder.Order
|
||||||
|
if err := db.First(&gotOrder, "id = ?", currentOrder.Id).Error; err != nil {
|
||||||
|
t.Fatalf("query updated order: %v", err)
|
||||||
|
}
|
||||||
|
if gotOrder.SubscriptionUserId != ownerUserID {
|
||||||
|
t.Fatalf("current order subscription_user_id = %d, want owner %d", gotOrder.SubscriptionUserId, ownerUserID)
|
||||||
|
}
|
||||||
|
if gotOrder.SubscribeToken != gotSub.Token {
|
||||||
|
t.Fatalf("current order subscribe_token = %q, want regenerated subscribe token %q", gotOrder.SubscribeToken, gotSub.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotUnlinkedOrder modelOrder.Order
|
||||||
|
if err := db.First(&gotUnlinkedOrder, "id = ?", unlinkedOrder.Id).Error; err != nil {
|
||||||
|
t.Fatalf("query unlinked order: %v", err)
|
||||||
|
}
|
||||||
|
if gotUnlinkedOrder.SubscriptionUserId != memberUserID {
|
||||||
|
t.Fatalf("unlinked order subscription_user_id = %d, want unchanged member %d", gotUnlinkedOrder.SubscriptionUserId, memberUserID)
|
||||||
|
}
|
||||||
|
if gotUnlinkedOrder.SubscribeToken != unlinkedOrder.SubscribeToken {
|
||||||
|
t.Fatalf("unlinked order subscribe_token = %q, want unchanged token %q", gotUnlinkedOrder.SubscribeToken, unlinkedOrder.SubscribeToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMoveMemberSubscribesToOwnerNoSubscribesIsNoop(t *testing.T) {
|
||||||
|
db := newFamilyBindingTestDB(t)
|
||||||
|
|
||||||
|
var moved []modelUser.Subscribe
|
||||||
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var err error
|
||||||
|
moved, err = moveMemberSubscribesToOwner(tx, 1001, 2001)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("move empty member subscribes: %v", err)
|
||||||
|
}
|
||||||
|
if len(moved) != 0 {
|
||||||
|
t.Fatalf("moved subscribes length = %d, want 0", len(moved))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferMemberSubscribesToOwnerStillDiscardsMemberSubscribes(t *testing.T) {
|
||||||
|
db := newFamilyBindingTestDB(t)
|
||||||
|
memberUserID := int64(1001)
|
||||||
|
ownerUserID := int64(2001)
|
||||||
|
now := time.Now().Add(-time.Hour)
|
||||||
|
|
||||||
|
order := modelOrder.Order{
|
||||||
|
Id: 9001,
|
||||||
|
UserId: memberUserID,
|
||||||
|
SubscriptionUserId: memberUserID,
|
||||||
|
OrderNo: "order-discard",
|
||||||
|
SubscribeToken: "token-discard",
|
||||||
|
Status: 5,
|
||||||
|
SubscribeId: 3001,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
sub := modelUser.Subscribe{
|
||||||
|
Id: 7001,
|
||||||
|
UserId: memberUserID,
|
||||||
|
OrderId: order.Id,
|
||||||
|
SubscribeId: order.SubscribeId,
|
||||||
|
StartTime: now,
|
||||||
|
ExpireTime: now.Add(30 * 24 * time.Hour),
|
||||||
|
Token: "token-discard",
|
||||||
|
UUID: "uuid-discard",
|
||||||
|
Status: 1,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
insertFamilyBindingTestOrder(t, db, order)
|
||||||
|
insertFamilyBindingTestSubscribe(t, db, sub)
|
||||||
|
|
||||||
|
var removed []modelUser.Subscribe
|
||||||
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var err error
|
||||||
|
removed, err = transferMemberSubscribesToOwner(tx, memberUserID, ownerUserID)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("discard member subscribes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(removed) != 1 {
|
||||||
|
t.Fatalf("removed subscribes length = %d, want 1", len(removed))
|
||||||
|
}
|
||||||
|
if removed[0].UserId != memberUserID {
|
||||||
|
t.Fatalf("removed cache copy user_id = %d, want original member %d", removed[0].UserId, memberUserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var memberCount int64
|
||||||
|
if err := db.Model(&modelUser.Subscribe{}).Where("user_id = ?", memberUserID).Count(&memberCount).Error; err != nil {
|
||||||
|
t.Fatalf("count member subscribes: %v", err)
|
||||||
|
}
|
||||||
|
if memberCount != 0 {
|
||||||
|
t.Fatalf("member subscribe count = %d, want 0", memberCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ownerCount int64
|
||||||
|
if err := db.Model(&modelUser.Subscribe{}).Where("user_id = ?", ownerUserID).Count(&ownerCount).Error; err != nil {
|
||||||
|
t.Fatalf("count owner subscribes: %v", err)
|
||||||
|
}
|
||||||
|
if ownerCount != 0 {
|
||||||
|
t.Fatalf("owner subscribe count = %d, want 0", ownerCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotOrder modelOrder.Order
|
||||||
|
if err := db.First(&gotOrder, "id = ?", order.Id).Error; err != nil {
|
||||||
|
t.Fatalf("query order: %v", err)
|
||||||
|
}
|
||||||
|
if gotOrder.SubscriptionUserId != memberUserID {
|
||||||
|
t.Fatalf("discard path order subscription_user_id = %d, want unchanged member %d", gotOrder.SubscriptionUserId, memberUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1193,7 +1193,7 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
activeSubscribe, err := l.svc.UserModel.FindActiveSubscribe(ctx, u.Id)
|
activeSubscribe, err := l.findGiftDaysSubscription(ctx, u.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
giftLog := &log.Gift{
|
giftLog := &log.Gift{
|
||||||
@@ -1215,9 +1215,18 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
now := time.Now()
|
||||||
if !activeSubscribe.ExpireTime.Equal(time.UnixMilli(0)) {
|
if !activeSubscribe.ExpireTime.Equal(time.UnixMilli(0)) {
|
||||||
|
if activeSubscribe.ExpireTime.Before(now) {
|
||||||
|
activeSubscribe.ExpireTime = now.Add(time.Duration(days) * 24 * time.Hour)
|
||||||
|
} else {
|
||||||
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
activeSubscribe.Status = 1
|
||||||
|
activeSubscribe.FinishedAt = nil
|
||||||
|
activeSubscribe.ExpiredDownload = 0
|
||||||
|
activeSubscribe.ExpiredUpload = 0
|
||||||
err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
|
err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1242,6 +1251,30 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *ActivateOrderLogic) findGiftDaysSubscription(ctx context.Context, userID int64) (*user.Subscribe, error) {
|
||||||
|
activeSubscribe, err := l.svc.UserModel.FindActiveSubscribe(ctx, userID)
|
||||||
|
if err == nil {
|
||||||
|
return activeSubscribe, nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fallback user.Subscribe
|
||||||
|
err = l.svc.DB.WithContext(ctx).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Where("user_id = ? AND token != ''", userID).
|
||||||
|
Where("status IN ?", []int64{0, 1, 2, 3}).
|
||||||
|
Order("expire_time DESC").
|
||||||
|
Order("updated_at DESC").
|
||||||
|
Order("id DESC").
|
||||||
|
First(&fallback).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &fallback, nil
|
||||||
|
}
|
||||||
|
|
||||||
// shouldProcessCommission determines if commission should be processed based on
|
// shouldProcessCommission determines if commission should be processed based on
|
||||||
// referrer existence, commission settings, and order type
|
// referrer existence, commission settings, and order type
|
||||||
func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool {
|
func (l *ActivateOrderLogic) shouldProcessCommission(userInfo *user.User, isFirstPurchase bool) bool {
|
||||||
@@ -1356,7 +1389,7 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
userSub, err := l.getUserSubscription(ctx, orderInfo.SubscribeToken)
|
userSub, err := l.resolveRenewalActivationSubscription(ctx, orderInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1378,7 +1411,7 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trigger user group recalculation (needed when renewing an expired subscription)
|
// Trigger user group recalculation (needed when renewing an expired subscription)
|
||||||
l.triggerUserGroupRecalculation(ctx, userInfo.Id)
|
l.triggerUserGroupRecalculation(ctx, userSub.UserId)
|
||||||
|
|
||||||
// Clear user subscription cache
|
// Clear user subscription cache
|
||||||
err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub)
|
err = l.svc.UserModel.ClearSubscribeCache(ctx, userSub)
|
||||||
@@ -1386,7 +1419,7 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
|||||||
logger.WithContext(ctx).Error("Clear user subscribe cache failed",
|
logger.WithContext(ctx).Error("Clear user subscribe cache failed",
|
||||||
logger.Field("error", err.Error()),
|
logger.Field("error", err.Error()),
|
||||||
logger.Field("subscribe_id", userSub.Id),
|
logger.Field("subscribe_id", userSub.Id),
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userSub.UserId),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1408,6 +1441,75 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *ActivateOrderLogic) resolveRenewalActivationSubscription(ctx context.Context, orderInfo *order.Order) (*user.Subscribe, error) {
|
||||||
|
if orderInfo == nil {
|
||||||
|
return nil, fmt.Errorf("renewal activation order is nil")
|
||||||
|
}
|
||||||
|
userSub, err := l.getUserSubscription(ctx, orderInfo.SubscribeToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if orderInfo.UserId <= 0 {
|
||||||
|
return userSub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedUserID := orderInfo.SubscriptionUserId
|
||||||
|
entitlement, err := commonLogic.ResolveEntitlementUser(ctx, l.svc.DB, orderInfo.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if entitlement != nil && entitlement.Source == commonLogic.EntitlementSourceFamilyOwner && entitlement.EffectiveUserID > 0 {
|
||||||
|
expectedUserID = entitlement.EffectiveUserID
|
||||||
|
}
|
||||||
|
if expectedUserID <= 0 {
|
||||||
|
if entitlement != nil && entitlement.EffectiveUserID > 0 {
|
||||||
|
expectedUserID = entitlement.EffectiveUserID
|
||||||
|
} else {
|
||||||
|
expectedUserID = orderInfo.UserId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userSub.UserId == expectedUserID {
|
||||||
|
return userSub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var target user.Subscribe
|
||||||
|
err = l.svc.DB.WithContext(ctx).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Where("user_id = ? AND subscribe_id = ? AND token != ''", expectedUserID, orderInfo.SubscribeId).
|
||||||
|
Where("status IN ?", []int64{0, 1, 2, 3}).
|
||||||
|
Order("expire_time DESC").
|
||||||
|
Order("updated_at DESC").
|
||||||
|
Order("id DESC").
|
||||||
|
First(&target).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.WithContext(ctx).Error("Resolve renewal activation target failed",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("order_no", orderInfo.OrderNo),
|
||||||
|
logger.Field("order_user_id", orderInfo.UserId),
|
||||||
|
logger.Field("expected_user_id", expectedUserID),
|
||||||
|
logger.Field("token_owner_user_id", userSub.UserId),
|
||||||
|
logger.Field("token_user_subscribe_id", userSub.Id),
|
||||||
|
logger.Field("subscribe_id", orderInfo.SubscribeId),
|
||||||
|
)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fmt.Errorf("renewal activation target mismatch: expected user %d subscription for subscribe %d not found", expectedUserID, orderInfo.SubscribeId)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "renewal_activation_target_redirected",
|
||||||
|
"[SubscriptionFlow] renewal activation redirected to entitlement owner subscription",
|
||||||
|
append(commonLogic.OrderTraceFields(orderInfo),
|
||||||
|
logger.Field("expected_user_id", expectedUserID),
|
||||||
|
logger.Field("original_user_subscribe_id", userSub.Id),
|
||||||
|
logger.Field("original_owner_user_id", userSub.UserId),
|
||||||
|
logger.Field("resolved_user_subscribe_id", target.Id),
|
||||||
|
logger.Field("resolved_owner_user_id", target.UserId),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
return &target, nil
|
||||||
|
}
|
||||||
|
|
||||||
// getUserSubscription retrieves user subscription by token
|
// getUserSubscription retrieves user subscription by token
|
||||||
func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token string) (*user.Subscribe, error) {
|
func (l *ActivateOrderLogic) getUserSubscription(ctx context.Context, token string) (*user.Subscribe, error) {
|
||||||
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, token)
|
userSub, err := l.svc.UserModel.FindOneSubscribeByToken(ctx, token)
|
||||||
|
|||||||
Reference in New Issue
Block a user