diff --git a/apis/admin/log.api b/apis/admin/log.api index 8575ca3..5210c68 100644 --- a/apis/admin/log.api +++ b/apis/admin/log.api @@ -149,6 +149,35 @@ type ( Total int64 `json:"total"` List []CommissionLog `json:"list"` } + OrderRefundLog { + OrderId int64 `json:"order_id"` + OrderNo string `json:"order_no"` + OperatorUserId int64 `json:"operator_user_id"` + OperatorAuthIdentifier string `json:"operator_auth_identifier,omitempty"` + TargetUserId int64 `json:"target_user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + RefererUserId int64 `json:"referer_user_id,omitempty"` + CommissionAmount int64 `json:"commission_amount"` + Reason string `json:"reason,omitempty"` + OrderStatusBefore uint8 `json:"order_status_before"` + OrderStatusAfter uint8 `json:"order_status_after"` + SubscribeStatusBefore uint8 `json:"subscribe_status_before"` + SubscribeStatusAfter uint8 `json:"subscribe_status_after"` + SubscribeExpireBefore int64 `json:"subscribe_expire_before"` + SubscribeExpireAfter int64 `json:"subscribe_expire_after"` + CommissionBefore int64 `json:"commission_before"` + CommissionAfter int64 `json:"commission_after"` + Timestamp int64 `json:"timestamp"` + } + FilterOrderRefundLogRequest { + FilterLogParams + OrderId int64 `form:"order_id,optional"` + UserId int64 `form:"user_id,optional"` + } + FilterOrderRefundLogResponse { + Total int64 `json:"total"` + List []OrderRefundLog `json:"list"` + } GiftLog { Type uint16 `json:"type"` userId int64 `json:"user_id"` @@ -291,6 +320,10 @@ service ppanel { @handler FilterCommissionLog get /commission/list (FilterCommissionLogRequest) returns (FilterCommissionLogResponse) + @doc "Filter order refund log" + @handler FilterOrderRefundLog + get /order/refund/list (FilterOrderRefundLogRequest) returns (FilterOrderRefundLogResponse) + @doc "Filter gift log" @handler FilterGiftLog get /gift/list (FilterGiftLogRequest) returns (FilterGiftLogResponse) diff --git a/apis/admin/order.api b/apis/admin/order.api index 4a5d12a..35a558f 100644 --- a/apis/admin/order.api +++ b/apis/admin/order.api @@ -33,6 +33,10 @@ type ( PaymentId int64 `json:"payment_id,omitempty"` TradeNo string `json:"trade_no,omitempty"` } + RefundOrderRequest { + Id int64 `json:"id" validate:"required"` + Reason string `json:"reason,omitempty" validate:"omitempty,max=500"` + } ActivateOrderRequest { OrderNo string `json:"order_no" validate:"required"` } @@ -68,6 +72,10 @@ service ppanel { @handler UpdateOrderStatus put /status (UpdateOrderStatusRequest) + @doc "Refund order" + @handler RefundOrder + post /refund (RefundOrderRequest) + @doc "Manually activate order" @handler ActivateOrder post /activate (ActivateOrderRequest) diff --git a/apis/types.api b/apis/types.api index bf185a4..609abab 100644 --- a/apis/types.api +++ b/apis/types.api @@ -426,6 +426,7 @@ type ( FeeAmount int64 `json:"fee_amount"` TradeNo string `json:"trade_no"` Status uint8 `json:"status"` + StatusName string `json:"status_name,omitempty"` SubscribeId int64 `json:"subscribe_id"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` @@ -448,6 +449,7 @@ type ( FeeAmount int64 `json:"fee_amount"` TradeNo string `json:"trade_no"` Status uint8 `json:"status"` + StatusName string `json:"status_name,omitempty"` SubscribeId int64 `json:"subscribe_id"` Subscribe Subscribe `json:"subscribe"` CreatedAt int64 `json:"created_at"` diff --git a/internal/handler/admin/log/filterOrderRefundLogHandler.go b/internal/handler/admin/log/filterOrderRefundLogHandler.go new file mode 100644 index 0000000..1d8f9e4 --- /dev/null +++ b/internal/handler/admin/log/filterOrderRefundLogHandler.go @@ -0,0 +1,25 @@ +package log + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +func FilterOrderRefundLogHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.FilterOrderRefundLogRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := log.NewFilterOrderRefundLogLogic(c.Request.Context(), svcCtx) + resp, err := l.FilterOrderRefundLog(&req) + result.HttpResult(c, resp, err) + } +} diff --git a/internal/handler/admin/order/refundOrderHandler.go b/internal/handler/admin/order/refundOrderHandler.go new file mode 100644 index 0000000..b916e6b --- /dev/null +++ b/internal/handler/admin/order/refundOrderHandler.go @@ -0,0 +1,25 @@ +package order + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/admin/order" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +func RefundOrderHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.RefundOrderRequest + _ = c.ShouldBind(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + + l := order.NewRefundOrderLogic(c.Request.Context(), svcCtx) + err := l.RefundOrder(&req) + result.HttpResult(c, nil, err) + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 0b35e68..67e9212 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -250,6 +250,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Filter commission log adminLogGroupRouter.GET("/commission/list", adminLog.FilterCommissionLogHandler(serverCtx)) + // Filter order refund log + adminLogGroupRouter.GET("/order/refund/list", adminLog.FilterOrderRefundLogHandler(serverCtx)) + // Filter email log adminLogGroupRouter.GET("/email/list", adminLog.FilterEmailLogHandler(serverCtx)) @@ -341,6 +344,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Update order status adminOrderGroupRouter.PUT("/status", adminOrder.UpdateOrderStatusHandler(serverCtx)) + // Refund order + adminOrderGroupRouter.POST("/refund", adminOrder.RefundOrderHandler(serverCtx)) + // Manually activate order adminOrderGroupRouter.POST("/activate", adminOrder.ActivateOrderHandler(serverCtx)) } diff --git a/internal/logic/admin/log/filterOrderRefundLogLogic.go b/internal/logic/admin/log/filterOrderRefundLogLogic.go new file mode 100644 index 0000000..508ecf4 --- /dev/null +++ b/internal/logic/admin/log/filterOrderRefundLogLogic.go @@ -0,0 +1,77 @@ +package log + +import ( + "context" + + modellog "github.com/perfect-panel/server/internal/model/log" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type FilterOrderRefundLogLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewFilterOrderRefundLogLogic(ctx context.Context, svcCtx *svc.ServiceContext) *FilterOrderRefundLogLogic { + return &FilterOrderRefundLogLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *FilterOrderRefundLogLogic) FilterOrderRefundLog(req *types.FilterOrderRefundLogRequest) (resp *types.FilterOrderRefundLogResponse, err error) { + data, total, err := l.svcCtx.LogModel.FilterSystemLog(l.ctx, &modellog.FilterParams{ + Page: req.Page, + Size: req.Size, + Data: req.Date, + Search: req.Search, + Type: modellog.TypeOrderRefund.Uint8(), + ObjectID: req.OrderId, + }) + if err != nil { + l.Errorw("Query order refund log failed", logger.Field("error", err.Error())) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Query order refund log failed") + } + + var list []types.OrderRefundLog + for _, item := range data { + var content modellog.OrderRefund + if err := content.Unmarshal([]byte(item.Content)); err != nil { + l.Errorf("unmarshal order refund log content failed: %v", err.Error()) + continue + } + if req.UserId > 0 && content.TargetUserId != req.UserId && content.RefererUserId != req.UserId && content.OperatorUserId != req.UserId { + continue + } + list = append(list, types.OrderRefundLog{ + OrderId: content.OrderId, + OrderNo: content.OrderNo, + OperatorUserId: content.OperatorUserId, + OperatorAuthIdentifier: content.OperatorAuthIdentifier, + TargetUserId: content.TargetUserId, + UserSubscribeId: content.UserSubscribeId, + RefererUserId: content.RefererUserId, + CommissionAmount: content.CommissionAmount, + Reason: content.Reason, + OrderStatusBefore: content.OrderStatusBefore, + OrderStatusAfter: content.OrderStatusAfter, + SubscribeStatusBefore: content.SubscribeStatusBefore, + SubscribeStatusAfter: content.SubscribeStatusAfter, + SubscribeExpireBefore: content.SubscribeExpireBefore, + SubscribeExpireAfter: content.SubscribeExpireAfter, + CommissionBefore: content.CommissionBefore, + CommissionAfter: content.CommissionAfter, + Timestamp: content.Timestamp, + }) + } + return &types.FilterOrderRefundLogResponse{ + Total: total, + List: list, + }, nil +} diff --git a/internal/logic/admin/order/getOrderListLogic.go b/internal/logic/admin/order/getOrderListLogic.go index cdf9da6..212b534 100644 --- a/internal/logic/admin/order/getOrderListLogic.go +++ b/internal/logic/admin/order/getOrderListLogic.go @@ -35,6 +35,28 @@ func (l *GetOrderListLogic) GetOrderList(req *types.GetOrderListRequest) (resp * resp = &types.GetOrderListResponse{} resp.List = make([]types.Order, 0) tool.DeepCopy(&resp.List, list) + for i := range resp.List { + resp.List[i].StatusName = orderStatusName(resp.List[i].Status) + } resp.Total = total return } + +func orderStatusName(status uint8) string { + switch status { + case 1: + return "pending" + case 2: + return "paid" + case 3: + return "closed" + case 4: + return "failed" + case 5: + return "finished" + case 6: + return "refunded" + default: + return "unknown" + } +} diff --git a/internal/logic/admin/order/refundOrderLogic.go b/internal/logic/admin/order/refundOrderLogic.go new file mode 100644 index 0000000..754091d --- /dev/null +++ b/internal/logic/admin/order/refundOrderLogic.go @@ -0,0 +1,298 @@ +package order + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/perfect-panel/server/internal/model/log" + 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" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +const ( + orderStatusRefunded = 6 +) + +type RefundOrderLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewRefundOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefundOrderLogic { + return &RefundOrderLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RefundOrderLogic) RefundOrder(req *types.RefundOrderRequest) error { + operator, ok := l.ctx.Value(constant.CtxKeyUser).(*modeluser.User) + if !ok { + return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") + } + + reason := strings.TrimSpace(req.Reason) + var cachesToClear []*modeluser.Subscribe + var planCacheIDs []int64 + var userCacheTargets []*modeluser.User + + err := l.svcCtx.DB.WithContext(l.ctx).Transaction(func(tx *gorm.DB) error { + var orderInfo modelorder.Order + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&modelorder.Order{}). + Where("id = ?", req.Id). + First(&orderInfo).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order %d not found", req.Id) + } + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query order failed: %v", err) + } + + if orderInfo.Status == orderStatusRefunded { + return errors.Wrapf(xerr.NewErrCode(xerr.OrderAlreadyRefunded), "order %d already refunded", orderInfo.Id) + } + if orderInfo.Status != 2 && orderInfo.Status != 5 { + return errors.Wrapf(xerr.NewErrCode(xerr.OrderStatusError), "order %d status %d is not refundable", orderInfo.Id, orderInfo.Status) + } + + userSub, err := l.lockRefundTargetSubscription(tx, &orderInfo) + if err != nil { + return err + } + + orderStatusBefore := orderInfo.Status + subStatusBefore := userSub.Status + subExpireBefore := userSub.ExpireTime + now := time.Now() + + referer, commissionAmount, err := l.lockCommissionSource(tx, orderInfo.OrderNo, orderInfo.Commission) + if err != nil { + return err + } + + var commissionBefore int64 + var commissionAfter int64 + if referer != nil { + commissionBefore = referer.Commission + commissionAfter = referer.Commission - commissionAmount + if err := tx.Model(&modeluser.User{}). + Where("id = ?", referer.Id). + UpdateColumn("commission", gorm.Expr("commission - ?", commissionAmount)).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund commission failed: %v", err) + } + + commissionLog := log.Commission{ + Type: log.CommissionTypeRefund, + Amount: -commissionAmount, + OrderNo: orderInfo.OrderNo, + Timestamp: now.UnixMilli(), + } + content, marshalErr := commissionLog.Marshal() + if marshalErr != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal commission refund log failed: %v", marshalErr) + } + if err := tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeCommission.Uint8(), + Date: now.Format(time.DateOnly), + ObjectID: referer.Id, + Content: string(content), + CreatedAt: now, + }).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert commission refund log failed: %v", err) + } + referer.Commission = commissionAfter + userCacheTargets = append(userCacheTargets, referer) + } + + if err := tx.Model(&modelorder.Order{}). + Where("id = ? AND status IN ?", orderInfo.Id, []uint8{2, 5}). + Update("status", orderStatusRefunded).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "refund order status update failed: %v", err) + } + orderInfo.Status = orderStatusRefunded + + userSub.Status = 3 + userSub.ExpireTime = now.Add(-time.Second) + userSub.FinishedAt = &now + if err := tx.Model(&modeluser.Subscribe{}). + Where("id = ?", userSub.Id). + Updates(map[string]interface{}{ + "status": userSub.Status, + "expire_time": userSub.ExpireTime, + "finished_at": userSub.FinishedAt, + }).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update subscription failed: %v", err) + } + + refundLog, err := l.buildRefundAuditLog(operator, &orderInfo, userSub, referer, commissionAmount, reason, orderStatusBefore, subStatusBefore, subExpireBefore, commissionBefore, commissionAfter, now) + if err != nil { + return err + } + logContent, err := refundLog.Marshal() + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "marshal refund audit log failed: %v", err) + } + if err := tx.Model(&log.SystemLog{}).Create(&log.SystemLog{ + Type: log.TypeOrderRefund.Uint8(), + Date: now.Format(time.DateOnly), + ObjectID: orderInfo.Id, + Content: string(logContent), + CreatedAt: now, + }).Error; err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert refund audit log failed: %v", err) + } + + cachesToClear = append(cachesToClear, userSub) + if userSub.SubscribeId > 0 { + planCacheIDs = append(planCacheIDs, userSub.SubscribeId) + } + if orderInfo.UserId > 0 { + orderUser := &modeluser.User{Id: orderInfo.UserId} + userCacheTargets = append(userCacheTargets, orderUser) + } + return nil + }) + if err != nil { + l.Errorw("[RefundOrder] refund failed", logger.Field("error", err.Error()), logger.Field("order_id", req.Id)) + return err + } + + if len(cachesToClear) > 0 { + if clearErr := l.svcCtx.UserModel.ClearSubscribeCache(l.ctx, cachesToClear...); clearErr != nil { + l.Errorw("[RefundOrder] clear subscribe cache failed", logger.Field("error", clearErr.Error()), logger.Field("order_id", req.Id)) + } + } + for _, subscribeID := range planCacheIDs { + if clearErr := l.svcCtx.SubscribeModel.ClearCache(l.ctx, subscribeID); clearErr != nil { + l.Errorw("[RefundOrder] clear subscribe cache failed", logger.Field("error", clearErr.Error()), logger.Field("subscribe_id", subscribeID)) + } + } + if len(userCacheTargets) > 0 { + if clearErr := l.svcCtx.UserModel.ClearUserCache(l.ctx, userCacheTargets...); clearErr != nil { + l.Errorw("[RefundOrder] clear user cache failed", logger.Field("error", clearErr.Error())) + } + } + return nil +} + +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 !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query refund subscription failed: %v", err) + } + + if orderInfo.Type != 2 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "order %d has no linked subscription", orderInfo.Id) + } + + if orderInfo.ParentId == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d has no parent order", orderInfo.Id) + } + + err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&modeluser.Subscribe{}). + Where("order_id = ?", orderInfo.ParentId). + First(&userSub).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, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query renewal subscription failed: %v", err) + } + return &userSub, nil +} + +func (l *RefundOrderLogic) lockCommissionSource(tx *gorm.DB, orderNo string, orderCommission int64) (*modeluser.User, int64, error) { + var commissionLogs []log.SystemLog + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&log.SystemLog{}). + Where("type = ? AND content LIKE ?", log.TypeCommission.Uint8(), fmt.Sprintf("%%\"order_no\":\"%s\"%%", orderNo)). + Order("id DESC"). + Find(&commissionLogs).Error; err != nil { + return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query commission log failed: %v", err) + } + + for _, item := range commissionLogs { + var content log.Commission + if err := content.Unmarshal([]byte(item.Content)); err != nil { + continue + } + if content.Type != log.CommissionTypePurchase && content.Type != log.CommissionTypeRenewal { + continue + } + + var referer modeluser.User + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&modeluser.User{}). + Where("id = ?", item.ObjectID). + First(&referer).Error; err != nil { + return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query commission owner failed: %v", err) + } + return &referer, content.Amount, nil + } + + if orderCommission > 0 { + return nil, 0, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundCommissionMismatch), "order %s commission owner not found", orderNo) + } + return nil, 0, nil +} + +func (l *RefundOrderLogic) buildRefundAuditLog( + operator *modeluser.User, + orderInfo *modelorder.Order, + userSub *modeluser.Subscribe, + referer *modeluser.User, + commissionAmount int64, + reason string, + orderStatusBefore uint8, + subStatusBefore uint8, + subExpireBefore time.Time, + commissionBefore int64, + commissionAfter int64, + now time.Time, +) (*log.OrderRefund, error) { + refundLog := &log.OrderRefund{ + OrderId: orderInfo.Id, + OrderNo: orderInfo.OrderNo, + OperatorUserId: operator.Id, + TargetUserId: userSub.UserId, + UserSubscribeId: userSub.Id, + CommissionAmount: commissionAmount, + Reason: reason, + OrderStatusBefore: orderStatusBefore, + OrderStatusAfter: orderInfo.Status, + SubscribeStatusBefore: subStatusBefore, + SubscribeStatusAfter: userSub.Status, + SubscribeExpireBefore: subExpireBefore.UnixMilli(), + SubscribeExpireAfter: userSub.ExpireTime.UnixMilli(), + CommissionBefore: commissionBefore, + CommissionAfter: commissionAfter, + Timestamp: now.UnixMilli(), + } + if referer != nil { + refundLog.RefererUserId = referer.Id + } + if len(operator.AuthMethods) > 0 { + refundLog.OperatorAuthIdentifier = operator.AuthMethods[0].AuthIdentifier + } + return refundLog, nil +} diff --git a/internal/logic/admin/order/refundOrderLogic_test.go b/internal/logic/admin/order/refundOrderLogic_test.go new file mode 100644 index 0000000..7c1d7dc --- /dev/null +++ b/internal/logic/admin/order/refundOrderLogic_test.go @@ -0,0 +1,85 @@ +package order + +import ( + "testing" + "time" + + modelorder "github.com/perfect-panel/server/internal/model/order" + modeluser "github.com/perfect-panel/server/internal/model/user" +) + +func TestOrderStatusName(t *testing.T) { + tests := map[uint8]string{ + 1: "pending", + 2: "paid", + 3: "closed", + 4: "failed", + 5: "finished", + 6: "refunded", + 7: "unknown", + } + + for input, want := range tests { + if got := orderStatusName(input); got != want { + t.Fatalf("status %d: got %q want %q", input, got, want) + } + } +} + +func TestBuildRefundAuditLog(t *testing.T) { + logic := &RefundOrderLogic{} + now := time.Unix(1710000000, 0) + expireBefore := now.Add(24 * time.Hour) + expireAfter := now.Add(-time.Second) + operator := &modeluser.User{ + Id: 100, + AuthMethods: []modeluser.AuthMethods{ + {AuthIdentifier: "admin@example.com"}, + }, + } + orderInfo := &modelorder.Order{Id: 10, OrderNo: "ORD-1", Status: orderStatusRefunded} + userSub := &modeluser.Subscribe{Id: 20, UserId: 30, Status: 3, ExpireTime: expireAfter} + referer := &modeluser.User{Id: 40} + + got, err := logic.buildRefundAuditLog( + operator, + orderInfo, + userSub, + referer, + 500, + "manual refund", + 5, + 1, + expireBefore, + 900, + 400, + now, + ) + if err != nil { + t.Fatalf("buildRefundAuditLog error: %v", err) + } + if got.OrderId != 10 || got.OrderNo != "ORD-1" { + t.Fatalf("unexpected order info: %+v", got) + } + if got.OperatorUserId != 100 || got.OperatorAuthIdentifier != "admin@example.com" { + t.Fatalf("unexpected operator info: %+v", got) + } + if got.TargetUserId != 30 || got.UserSubscribeId != 20 { + t.Fatalf("unexpected subscription info: %+v", got) + } + if got.RefererUserId != 40 || got.CommissionAmount != 500 { + t.Fatalf("unexpected commission info: %+v", got) + } + if got.OrderStatusBefore != 5 || got.OrderStatusAfter != orderStatusRefunded { + t.Fatalf("unexpected order status: %+v", got) + } + if got.SubscribeStatusBefore != 1 || got.SubscribeStatusAfter != 3 { + t.Fatalf("unexpected subscribe status: %+v", got) + } + if got.SubscribeExpireBefore != expireBefore.UnixMilli() || got.SubscribeExpireAfter != expireAfter.UnixMilli() { + t.Fatalf("unexpected expire transition: %+v", got) + } + if got.CommissionBefore != 900 || got.CommissionAfter != 400 { + t.Fatalf("unexpected commission transition: %+v", got) + } +} diff --git a/internal/model/log/log.go b/internal/model/log/log.go index 1afd13d..a3cc36c 100644 --- a/internal/model/log/log.go +++ b/internal/model/log/log.go @@ -23,6 +23,7 @@ const ( TypeSubscribeTraffic Type = 21 // Subscription traffic log TypeServerTraffic Type = 22 // Server traffic log TypeResetSubscribe Type = 23 // Reset subscription log + TypeOrderRefund Type = 24 // Admin order refund log TypeLogin Type = 30 // Login log TypeRegister Type = 31 // Registration log TypeBalance Type = 32 // Balance log @@ -277,6 +278,42 @@ func (c *Commission) Unmarshal(data []byte) error { return json.Unmarshal(data, aux) } +type OrderRefund struct { + OrderId int64 `json:"order_id"` + OrderNo string `json:"order_no"` + OperatorUserId int64 `json:"operator_user_id"` + OperatorAuthIdentifier string `json:"operator_auth_identifier,omitempty"` + TargetUserId int64 `json:"target_user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + RefererUserId int64 `json:"referer_user_id,omitempty"` + CommissionAmount int64 `json:"commission_amount"` + Reason string `json:"reason,omitempty"` + OrderStatusBefore uint8 `json:"order_status_before"` + OrderStatusAfter uint8 `json:"order_status_after"` + SubscribeStatusBefore uint8 `json:"subscribe_status_before"` + SubscribeStatusAfter uint8 `json:"subscribe_status_after"` + SubscribeExpireBefore int64 `json:"subscribe_expire_before"` + SubscribeExpireAfter int64 `json:"subscribe_expire_after"` + CommissionBefore int64 `json:"commission_before"` + CommissionAfter int64 `json:"commission_after"` + Timestamp int64 `json:"timestamp"` +} + +func (o *OrderRefund) Marshal() ([]byte, error) { + type Alias OrderRefund + return json.Marshal(&struct { + *Alias + }{ + Alias: (*Alias)(o), + }) +} + +func (o *OrderRefund) Unmarshal(data []byte) error { + type Alias OrderRefund + aux := (*Alias)(o) + return json.Unmarshal(data, aux) +} + // Gift represents a gift log entry. type Gift struct { Type uint16 `json:"type"` diff --git a/internal/types/types.go b/internal/types/types.go index 663ceeb..2deb523 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -7,6 +7,11 @@ type ActivateOrderRequest struct { OrderNo string `json:"order_no" validate:"required"` } +type RefundOrderRequest struct { + Id int64 `json:"id" validate:"required"` + Reason string `json:"reason,omitempty" validate:"omitempty,max=500"` +} + type Ads struct { Id int `json:"id"` Title string `json:"title"` @@ -826,6 +831,17 @@ type FilterCommissionLogResponse struct { List []CommissionLog `json:"list"` } +type FilterOrderRefundLogRequest struct { + FilterLogParams + OrderId int64 `form:"order_id,optional"` + UserId int64 `form:"user_id,optional"` +} + +type FilterOrderRefundLogResponse struct { + Total int64 `json:"total"` + List []OrderRefundLog `json:"list"` +} + type FilterEmailLogResponse struct { Total int64 `json:"total"` List []MessageLog `json:"list"` @@ -1852,6 +1868,7 @@ type Order struct { FeeAmount int64 `json:"fee_amount"` TradeNo string `json:"trade_no"` Status uint8 `json:"status"` + StatusName string `json:"status_name,omitempty"` SubscribeId int64 `json:"subscribe_id"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` @@ -1875,12 +1892,34 @@ type OrderDetail struct { FeeAmount int64 `json:"fee_amount"` TradeNo string `json:"trade_no"` Status uint8 `json:"status"` + StatusName string `json:"status_name,omitempty"` SubscribeId int64 `json:"subscribe_id"` Subscribe Subscribe `json:"subscribe"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } +type OrderRefundLog struct { + OrderId int64 `json:"order_id"` + OrderNo string `json:"order_no"` + OperatorUserId int64 `json:"operator_user_id"` + OperatorAuthIdentifier string `json:"operator_auth_identifier,omitempty"` + TargetUserId int64 `json:"target_user_id"` + UserSubscribeId int64 `json:"user_subscribe_id"` + RefererUserId int64 `json:"referer_user_id,omitempty"` + CommissionAmount int64 `json:"commission_amount"` + Reason string `json:"reason,omitempty"` + OrderStatusBefore uint8 `json:"order_status_before"` + OrderStatusAfter uint8 `json:"order_status_after"` + SubscribeStatusBefore uint8 `json:"subscribe_status_before"` + SubscribeStatusAfter uint8 `json:"subscribe_status_after"` + SubscribeExpireBefore int64 `json:"subscribe_expire_before"` + SubscribeExpireAfter int64 `json:"subscribe_expire_after"` + CommissionBefore int64 `json:"commission_before"` + CommissionAfter int64 `json:"commission_after"` + Timestamp int64 `json:"timestamp"` +} + type OrdersStatistics struct { Date string `json:"date,omitempty"` AmountTotal int64 `json:"amount_total"` diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index e334a71..d6476e3 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -140,9 +140,12 @@ const ( ) const ( - OrderNotExist uint32 = 61001 - PaymentMethodNotFound uint32 = 61002 - OrderStatusError uint32 = 61003 - InsufficientOfPeriod uint32 = 61004 - ExistAvailableTraffic uint32 = 61005 + OrderNotExist uint32 = 61001 + PaymentMethodNotFound uint32 = 61002 + OrderStatusError uint32 = 61003 + InsufficientOfPeriod uint32 = 61004 + ExistAvailableTraffic uint32 = 61005 + OrderAlreadyRefunded uint32 = 61006 + OrderRefundNoSubscription uint32 = 61007 + OrderRefundCommissionMismatch uint32 = 61008 ) diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index 3889559..b4793c2 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -98,10 +98,13 @@ func init() { UseridNotMatch: "Userid not match", // Order error - OrderNotExist: "Order does not exist", - PaymentMethodNotFound: "Payment method not found", - OrderStatusError: "Order status error", - InsufficientOfPeriod: "Insufficient number of period", + OrderNotExist: "Order does not exist", + PaymentMethodNotFound: "Payment method not found", + OrderStatusError: "Order status error", + InsufficientOfPeriod: "Insufficient number of period", + OrderAlreadyRefunded: "Order already refunded", + OrderRefundNoSubscription: "Refund target subscription not found", + OrderRefundCommissionMismatch: "Refund commission source not found", // Permission error PermissionDenied: "Permission denied",