From b98d718f3c79598a82799f637efafd3a4cb00e0a Mon Sep 17 00:00:00 2001 From: shanshanzhong147 Date: Sun, 7 Jun 2026 08:23:19 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(#11):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AE=B6=E5=BA=AD=E7=BB=84=E8=AE=A2=E5=8D=95=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E5=BD=92=E5=B1=9E=20(#13)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- .../logic/admin/order/refundOrderLogic.go | 136 +++++++++++++++-- .../admin/order/refundOrderLogic_test.go | 137 +++++++++++++++++- 2 files changed, 258 insertions(+), 15 deletions(-) diff --git a/internal/logic/admin/order/refundOrderLogic.go b/internal/logic/admin/order/refundOrderLogic.go index feb105c..ccb6daf 100644 --- a/internal/logic/admin/order/refundOrderLogic.go +++ b/internal/logic/admin/order/refundOrderLogic.go @@ -173,6 +173,9 @@ func (l *RefundOrderLogic) RefundOrder(req *types.RefundOrderRequest) error { orderUser := &modeluser.User{Id: orderInfo.UserId} userCacheTargets = append(userCacheTargets, orderUser) } + if userSub.UserId > 0 && userSub.UserId != orderInfo.UserId { + userCacheTargets = append(userCacheTargets, &modeluser.User{Id: userSub.UserId}) + } return nil }) if err != nil { @@ -199,16 +202,16 @@ func (l *RefundOrderLogic) RefundOrder(req *types.RefundOrderRequest) error { } func (l *RefundOrderLogic) lockRefundTargetSubscription(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) { - var userSub modeluser.Subscribe - err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). - Model(&modeluser.Subscribe{}). - Where("order_id = ?", orderInfo.Id). - First(&userSub).Error - if err == nil { - return &userSub, nil + if userSub, err := l.lockSubscriptionByOrderID(tx, orderInfo.Id); err != nil { + return nil, err + } else if userSub != nil { + return userSub, nil } - if !errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query refund subscription failed: %v", err) + + if userSub, err := l.lockSubscriptionByEntitlement(tx, orderInfo); err != nil { + return nil, err + } else if userSub != nil { + return userSub, nil } if orderInfo.Type != 2 { @@ -219,17 +222,122 @@ func (l *RefundOrderLogic) lockRefundTargetSubscription(tx *gorm.DB, orderInfo * return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d has no parent order", orderInfo.Id) } - err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). + if userSub, err := l.lockSubscriptionByOrderID(tx, orderInfo.ParentId); err != nil { + return nil, err + } else if userSub != nil { + return userSub, nil + } + + if userSub, err := l.lockRenewalParentEntitlementSubscription(tx, orderInfo); err != nil { + return nil, err + } else if userSub != nil { + return userSub, nil + } + + return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d parent subscription not found", orderInfo.Id) +} + +func (l *RefundOrderLogic) lockSubscriptionByOrderID(tx *gorm.DB, orderID int64) (*modeluser.Subscribe, error) { + if orderID <= 0 { + return nil, nil + } + + var userSub modeluser.Subscribe + err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Model(&modeluser.Subscribe{}). - Where("order_id = ?", orderInfo.ParentId). + Where("order_id = ?", orderID). First(&userSub).Error + if err == nil { + return &userSub, nil + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query refund subscription failed: %v", err) +} + +func (l *RefundOrderLogic) lockSubscriptionByEntitlement(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) { + entitlementUserID := orderInfo.SubscriptionUserId + if entitlementUserID == 0 { + entitlementUserID = orderInfo.UserId + } + if entitlementUserID <= 0 { + return nil, nil + } + + if userSub, err := l.lockExactEntitlementSubscription(tx, orderInfo, entitlementUserID); err != nil { + return nil, err + } else if userSub != nil { + return userSub, nil + } + + var userSub modeluser.Subscribe + err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&modeluser.Subscribe{}). + Where("user_id = ? AND subscribe_id = ?", entitlementUserID, orderInfo.SubscribeId). + Where("status IN ?", []int64{0, 1, 2, 3, 5}). + Order("expire_time DESC"). + Order("updated_at DESC"). + Order("id DESC"). + First(&userSub).Error + if err == nil { + return &userSub, nil + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query entitlement subscription failed: %v", err) +} + +func (l *RefundOrderLogic) lockExactEntitlementSubscription(tx *gorm.DB, orderInfo *modelorder.Order, entitlementUserID int64) (*modeluser.Subscribe, error) { + if orderInfo.Id <= 0 && orderInfo.SubscribeToken == "" { + return nil, nil + } + + query := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&modeluser.Subscribe{}). + Where("user_id = ? AND subscribe_id = ?", entitlementUserID, orderInfo.SubscribeId) + if orderInfo.Id > 0 && orderInfo.SubscribeToken != "" { + query = query.Where("(order_id = ? OR token = ?)", orderInfo.Id, orderInfo.SubscribeToken) + } else if orderInfo.Id > 0 { + query = query.Where("order_id = ?", orderInfo.Id) + } else { + query = query.Where("token = ?", orderInfo.SubscribeToken) + } + + var userSub modeluser.Subscribe + err := query.Order("id DESC").First(&userSub).Error + if err == nil { + return &userSub, nil + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query exact entitlement subscription failed: %v", err) +} + +func (l *RefundOrderLogic) lockRenewalParentEntitlementSubscription(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) { + var parentOrder modelorder.Order + err := tx.Model(&modelorder.Order{}). + Where("id = ?", orderInfo.ParentId). + First(&parentOrder).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d parent subscription not found", orderInfo.Id) + return nil, nil } - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query renewal subscription failed: %v", err) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query renewal parent order failed: %v", err) } - return &userSub, nil + + if parentOrder.SubscriptionUserId == 0 && orderInfo.SubscriptionUserId > 0 { + parentOrder.SubscriptionUserId = orderInfo.SubscriptionUserId + } + if parentOrder.UserId == 0 { + parentOrder.UserId = orderInfo.UserId + } + if parentOrder.SubscribeId == 0 { + parentOrder.SubscribeId = orderInfo.SubscribeId + } + return l.lockSubscriptionByEntitlement(tx, &parentOrder) } func (l *RefundOrderLogic) lockCommissionSource(tx *gorm.DB, orderNo string, orderCommission int64) (*modeluser.User, int64, error) { diff --git a/internal/logic/admin/order/refundOrderLogic_test.go b/internal/logic/admin/order/refundOrderLogic_test.go index e673f15..a7cabff 100644 --- a/internal/logic/admin/order/refundOrderLogic_test.go +++ b/internal/logic/admin/order/refundOrderLogic_test.go @@ -8,9 +8,9 @@ import ( "time" "github.com/DATA-DOG/go-sqlmock" - "github.com/perfect-panel/server/internal/svc" modelorder "github.com/perfect-panel/server/internal/model/order" 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/constant" "github.com/perfect-panel/server/pkg/logger" @@ -195,6 +195,141 @@ func TestRefundOrder_RejectsWhenStatusNotRefundable(t *testing.T) { } } +func TestLockRefundTargetSubscription_FamilyMemberPurchaseLocksOwnerSubscription(t *testing.T) { + const ( + orderID = int64(2001) + memberUID = int64(101) + ownerUID = int64(201) + subscribeID = int64(301) + userSubID = int64(401) + ) + + db, mock, cleanup := newRefundOrderTestDB(t) + defer cleanup() + + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(orderID, 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(ownerUID, subscribeID, orderID, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "order_id", "subscribe_id", "status", "expire_time"}). + AddRow(userSubID, ownerUID, orderID, subscribeID, uint8(1), time.Now().Add(24*time.Hour))) + + logic := &RefundOrderLogic{} + got, err := logic.lockRefundTargetSubscription(db, &modelorder.Order{ + Id: orderID, + UserId: memberUID, + SubscriptionUserId: ownerUID, + Type: 1, + SubscribeId: subscribeID, + }) + if err != nil { + t.Fatalf("lockRefundTargetSubscription error: %v", err) + } + if got.Id != userSubID || got.UserId != ownerUID { + t.Fatalf("locked subscription = %+v, want id=%d user_id=%d", got, userSubID, ownerUID) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestLockRefundTargetSubscription_RenewalFallsBackToParentOwnerEntitlement(t *testing.T) { + const ( + orderID = int64(2101) + parentOrderID = int64(2100) + memberUID = int64(111) + ownerUID = int64(211) + subscribeID = int64(311) + userSubID = int64(411) + ) + + db, mock, cleanup := newRefundOrderTestDB(t) + defer cleanup() + + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(orderID, 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(ownerUID, subscribeID, orderID, "renew-token", 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(ownerUID, subscribeID, int64(0), int64(1), int64(2), int64(3), int64(5), 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(parentOrderID, 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `order`"). + WithArgs(parentOrderID, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subscription_user_id", "subscribe_id", "subscribe_token"}). + AddRow(parentOrderID, memberUID, ownerUID, subscribeID, "owner-token")) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(ownerUID, subscribeID, parentOrderID, "owner-token", 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "order_id", "subscribe_id", "status", "expire_time", "token"}). + AddRow(userSubID, ownerUID, parentOrderID, subscribeID, uint8(1), time.Now().Add(24*time.Hour), "owner-token")) + + logic := &RefundOrderLogic{} + got, err := logic.lockRefundTargetSubscription(db, &modelorder.Order{ + Id: orderID, + ParentId: parentOrderID, + UserId: memberUID, + SubscriptionUserId: ownerUID, + Type: 2, + SubscribeId: subscribeID, + SubscribeToken: "renew-token", + }) + if err != nil { + t.Fatalf("lockRefundTargetSubscription error: %v", err) + } + if got.Id != userSubID || got.UserId != ownerUID || got.OrderId != parentOrderID { + t.Fatalf("locked subscription = %+v, want id=%d user_id=%d order_id=%d", got, userSubID, ownerUID, parentOrderID) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + +func TestRefundOrder_NoTargetSubscriptionDoesNotRefundOrder(t *testing.T) { + const ( + orderID = int64(2201) + orderNo = "ORD-NO-SUB" + operatorUID = int64(519) + userID = int64(7001) + subscribeID = int64(8001) + ) + + db, mock, cleanup := newRefundOrderTestDB(t) + defer cleanup() + + mock.ExpectBegin() + mock.ExpectQuery("FROM `order`"). + WithArgs(orderID, 1). + WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "status", "type", "commission", "user_id", "subscribe_id"}). + AddRow(orderID, orderNo, uint8(5), uint8(1), int64(0), userID, subscribeID)) + mock.ExpectQuery("FROM `system_logs`"). + WithArgs(uint8(33), fmt.Sprintf(`%%"order_no":"%s"%%`, orderNo)). + WillReturnRows(sqlmock.NewRows([]string{"id", "content"})) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(orderID, 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(userID, subscribeID, orderID, 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectQuery("FROM `user_subscribe`"). + WithArgs(userID, subscribeID, int64(0), int64(1), int64(2), int64(3), int64(5), 1). + WillReturnError(gorm.ErrRecordNotFound) + mock.ExpectRollback() + + logic := newTestRefundOrderLogic(t, db, operatorUID) + err := logic.RefundOrder(&types.RefundOrderRequest{Id: orderID, Reason: "missing subscription"}) + if !isErrCode(err, xerr.OrderRefundNoSubscription) { + t.Fatalf("RefundOrder error code = %v, want OrderRefundNoSubscription; raw=%v", errCodeOf(err), err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("unmet sql expectations: %v", err) + } +} + func newRefundOrderTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) { t.Helper()