diff --git a/debug_device_login b/debug_device_login new file mode 100755 index 0000000..3e898aa Binary files /dev/null and b/debug_device_login differ diff --git a/internal/logic/common/subscriptionTrace.go b/internal/logic/common/subscriptionTrace.go new file mode 100644 index 0000000..267b956 --- /dev/null +++ b/internal/logic/common/subscriptionTrace.go @@ -0,0 +1,108 @@ +package common + +import ( + "strings" + + ordermodel "github.com/perfect-panel/server/internal/model/order" + usermodel "github.com/perfect-panel/server/internal/model/user" + "github.com/perfect-panel/server/pkg/logger" +) + +const ( + SubscriptionTraceType = "subscription_flow" + SubscriptionTraceFlowOrder = "order_subscription" + SubscriptionTraceFlowEmailBind = "email_bind_subscription" +) + +func SubscriptionTraceFields(flow string, stage string, fields ...logger.LogField) []logger.LogField { + base := []logger.LogField{ + logger.Field("trace_type", SubscriptionTraceType), + logger.Field("flow", flow), + logger.Field("stage", stage), + } + + return append(base, fields...) +} + +func SubscriptionTraceInfo(log logger.Logger, flow string, stage string, msg string, fields ...logger.LogField) { + log.Infow(msg, SubscriptionTraceFields(flow, stage, fields...)...) +} + +func SubscriptionTraceError(log logger.Logger, flow string, stage string, msg string, fields ...logger.LogField) { + log.Errorw(msg, SubscriptionTraceFields(flow, stage, fields...)...) +} + +func OrderTraceFields(orderInfo *ordermodel.Order) []logger.LogField { + if orderInfo == nil { + return nil + } + + effectiveUserID := orderInfo.UserId + if orderInfo.SubscriptionUserId > 0 { + effectiveUserID = orderInfo.SubscriptionUserId + } + + fields := []logger.LogField{ + logger.Field("order_id", orderInfo.Id), + logger.Field("order_no", orderInfo.OrderNo), + logger.Field("order_type", orderInfo.Type), + logger.Field("order_status", orderInfo.Status), + logger.Field("user_id", orderInfo.UserId), + logger.Field("subscription_user_id", orderInfo.SubscriptionUserId), + logger.Field("effective_user_id", effectiveUserID), + logger.Field("order_subscribe_id", orderInfo.SubscribeId), + logger.Field("payment_id", orderInfo.PaymentId), + logger.Field("payment_method", orderInfo.Method), + logger.Field("parent_order_id", orderInfo.ParentId), + logger.Field("quantity", orderInfo.Quantity), + logger.Field("is_new_order", orderInfo.IsNew), + } + + if tail := SensitiveTail(orderInfo.SubscribeToken); tail != "" { + fields = append(fields, logger.Field("subscribe_token_tail", tail)) + } + if tail := SensitiveTail(orderInfo.TradeNo); tail != "" { + fields = append(fields, logger.Field("trade_no_tail", tail)) + } + if tail := SensitiveTail(orderInfo.AppAccountToken); tail != "" { + fields = append(fields, logger.Field("app_account_token_tail", tail)) + } + + return fields +} + +func UserSubscribeTraceFields(userSub *usermodel.Subscribe) []logger.LogField { + if userSub == nil { + return nil + } + + fields := []logger.LogField{ + logger.Field("user_subscribe_id", userSub.Id), + logger.Field("subscribe_owner_user_id", userSub.UserId), + logger.Field("user_subscribe_plan_id", userSub.SubscribeId), + logger.Field("subscribe_order_id", userSub.OrderId), + logger.Field("subscribe_status", userSub.Status), + logger.Field("expire_time", userSub.ExpireTime), + } + + if tail := SensitiveTail(userSub.Token); tail != "" { + fields = append(fields, logger.Field("subscribe_token_tail", tail)) + } + if tail := SensitiveTail(userSub.UUID); tail != "" { + fields = append(fields, logger.Field("subscribe_uuid_tail", tail)) + } + + return fields +} + +func SensitiveTail(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + if len(value) <= 8 { + return value + } + + return value[len(value)-8:] +} diff --git a/internal/logic/notify/alipayNotifyLogic.go b/internal/logic/notify/alipayNotifyLogic.go index f3ff752..283d64d 100644 --- a/internal/logic/notify/alipayNotifyLogic.go +++ b/internal/logic/notify/alipayNotifyLogic.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/xerr" @@ -56,6 +57,12 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error { l.Logger.Error("[AlipayNotify] Decode notification failed", logger.Field("error", err.Error())) return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received", + "[SubscriptionFlow] alipay notify received", + logger.Field("order_no", notify.OrderNo), + logger.Field("payment_platform", data.Platform), + logger.Field("notify_status", string(notify.Status)), + ) if notify.Status == alipay.Success { orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, notify.OrderNo) if err != nil { @@ -73,6 +80,12 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error { l.Logger.Error("[AlipayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo)) return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled", + "[SubscriptionFlow] alipay notify marked order as paid", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", data.Platform), + )..., + ) l.Logger.Info("[AlipayNotify] Notify status success", logger.Field("orderNo", notify.OrderNo)) payload := types.ForthwithActivateOrderPayload{ OrderNo: notify.OrderNo, @@ -88,6 +101,13 @@ func (l *AlipayNotifyLogic) AlipayNotify(r *http.Request) error { l.Logger.Error("[AlipayNotify] Enqueue task failed", logger.Field("error", err.Error())) return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued", + "[SubscriptionFlow] activation task enqueued from alipay notify", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", data.Platform), + logger.Field("queue_task_id", taskInfo.ID), + )..., + ) l.Logger.Info("[AlipayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo)) } else { l.Logger.Error("[AlipayNotify] Notify status failed", logger.Field("status", string(notify.Status))) diff --git a/internal/logic/notify/appleIAPNotifyLogic.go b/internal/logic/notify/appleIAPNotifyLogic.go index 19e25f8..ed2944b 100644 --- a/internal/logic/notify/appleIAPNotifyLogic.go +++ b/internal/logic/notify/appleIAPNotifyLogic.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + commonLogic "github.com/perfect-panel/server/internal/logic/common" iapmodel "github.com/perfect-panel/server/internal/model/iap/apple" "github.com/perfect-panel/server/internal/model/subscribe" "github.com/perfect-panel/server/internal/model/user" @@ -57,6 +58,13 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error { } // 验签通过,记录通知类型与关键交易标识 l.Infow("iap notify verified", logger.Field("type", ntype), logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received", + "[SubscriptionFlow] apple iap server notification received", + logger.Field("notify_type", ntype), + logger.Field("product_id", txPayload.ProductId), + logger.Field("original_transaction_tail", commonLogic.SensitiveTail(txPayload.OriginalTransactionId)), + logger.Field("transaction_id_tail", commonLogic.SensitiveTail(txPayload.TransactionId)), + ) return l.svcCtx.DB.Transaction(func(db *gorm.DB) error { var existing *iapmodel.Transaction existing, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId) @@ -201,6 +209,13 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error { return err } l.Infow("iap notify fallback updated subscribe", logger.Field("userSubscribeId", candidate.Id), logger.Field("status", candidate.Status)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_updated_from_notify", + "[SubscriptionFlow] apple iap notify updated fallback subscription candidate", + append(commonLogic.UserSubscribeTraceFields(candidate), + logger.Field("notify_type", ntype), + logger.Field("product_id", txPayload.ProductId), + )..., + ) break } } @@ -226,6 +241,13 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error { } // 更新成功,输出订阅状态 l.Infow("iap notify updated subscribe", logger.Field("userSubscribeId", sub.Id), logger.Field("status", sub.Status)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_updated_from_notify", + "[SubscriptionFlow] apple iap notify updated subscription", + append(commonLogic.UserSubscribeTraceFields(sub), + logger.Field("notify_type", ntype), + logger.Field("product_id", txPayload.ProductId), + )..., + ) } return nil }) diff --git a/internal/logic/notify/ePayNotifyLogic.go b/internal/logic/notify/ePayNotifyLogic.go index 0fd33cc..d9a1bd5 100644 --- a/internal/logic/notify/ePayNotifyLogic.go +++ b/internal/logic/notify/ePayNotifyLogic.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/url" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/xerr" @@ -44,12 +45,18 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { l.Logger.Error("[EPayNotify] Payment not found in context") return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment config not found") } - l.Infof("[EPayNotify] Payment config: %+v", data) orderInfo, err := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OutTradeNo) if err != nil { l.Logger.Error("[EPayNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo)) return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo) } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received", + "[SubscriptionFlow] epay notify received", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", data.Platform), + logger.Field("trade_status", req.TradeStatus), + )..., + ) var config payment.EPayConfig if err := json.Unmarshal([]byte(data.Config), &config); err != nil { @@ -75,6 +82,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo)) return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled", + "[SubscriptionFlow] epay notify marked order as paid", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", data.Platform), + )..., + ) // Create activate order task payload := queueType.ForthwithActivateOrderPayload{ OrderNo: req.OutTradeNo, @@ -90,6 +103,13 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error())) return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued", + "[SubscriptionFlow] activation task enqueued from epay notify", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", data.Platform), + logger.Field("queue_task_id", taskInfo.ID), + )..., + ) l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo)) return nil } diff --git a/internal/logic/notify/stripeNotifyLogic.go b/internal/logic/notify/stripeNotifyLogic.go index 47b3d05..8becc18 100644 --- a/internal/logic/notify/stripeNotifyLogic.go +++ b/internal/logic/notify/stripeNotifyLogic.go @@ -6,6 +6,7 @@ import ( "io" "net/http" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/xerr" @@ -67,6 +68,13 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter) l.Logger.Error("[StripeNotify] Find order failed", logger.Field("error", err.Error()), logger.Field("orderNo", notify.OrderNo)) return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", notify.OrderNo) } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_notify_received", + "[SubscriptionFlow] stripe notify received", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", stripeConfig.Platform), + logger.Field("stripe_event_type", notify.EventType), + )..., + ) if notify.EventType == "payment_intent.succeeded" { if orderInfo.Status == 5 { return nil @@ -76,6 +84,13 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter) if err != nil { return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled", + "[SubscriptionFlow] stripe notify marked order as paid", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", stripeConfig.Platform), + logger.Field("stripe_event_type", notify.EventType), + )..., + ) // create ActivateOrder task payload := types.ForthwithActivateOrderPayload{ OrderNo: notify.OrderNo, @@ -86,11 +101,19 @@ func (l *StripeNotifyLogic) StripeNotify(r *http.Request, w http.ResponseWriter) return err } task := asynq.NewTask(types.ForthwithActivateOrder, bytes, asynq.MaxRetry(5)) - _, err = l.svcCtx.Queue.Enqueue(task) + taskInfo, err := l.svcCtx.Queue.Enqueue(task) if err != nil { l.Errorw("[StripeNotify] Enqueue error", logger.Field("errors", err.Error())) return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued", + "[SubscriptionFlow] activation task enqueued from stripe notify", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", stripeConfig.Platform), + logger.Field("stripe_event_type", notify.EventType), + logger.Field("queue_task_id", taskInfo.ID), + )..., + ) l.Infow("[StripeNotify] success", logger.Field("orderNo", notify.OrderNo)) } return nil diff --git a/internal/logic/public/iap/apple/attachTransactionLogic.go b/internal/logic/public/iap/apple/attachTransactionLogic.go index e004de6..4ff14f7 100644 --- a/internal/logic/public/iap/apple/attachTransactionLogic.go +++ b/internal/logic/public/iap/apple/attachTransactionLogic.go @@ -82,6 +82,13 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest l.Errorw("订单与当前用户不匹配", logger.Field("orderNo", req.OrderNo), logger.Field("orderUserId", orderInfo.UserId), logger.Field("userId", u.Id)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order owner mismatch") } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "iap_attach_start", + "[SubscriptionFlow] apple iap attach flow started", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("request_user_id", u.Id), + logger.Field("effective_user_id", entitlement.EffectiveUserID), + )..., + ) isNewPurchaseOrder := orderInfo.Type == orderTypeSubscribe if isNewPurchaseOrder { l.Infow("首购订单将只由订单激活流程创建订阅", logger.Field("orderNo", req.OrderNo), logger.Field("orderType", orderInfo.Type)) @@ -93,6 +100,14 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws") } l.Infow("JWS 验签成功", logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("purchaseAt", txPayload.PurchaseDate)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "iap_attach_verified", + "[SubscriptionFlow] apple iap transaction verified", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("product_id", txPayload.ProductId), + logger.Field("original_transaction_tail", commonLogic.SensitiveTail(txPayload.OriginalTransactionId)), + logger.Field("transaction_id_tail", commonLogic.SensitiveTail(txPayload.TransactionId)), + )..., + ) tradeNoCandidates := l.getAppleTradeNoCandidates(txPayload) existingOrderNo, validateErr := l.validateOrderTradeNoBinding(orderInfo, tradeNoCandidates) if validateErr != nil { @@ -390,6 +405,12 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest return e } l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix())) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "subscription_created", + "[SubscriptionFlow] apple iap attach created a subscription placeholder before queue activation", + append(commonLogic.OrderTraceFields(orderInfo), + commonLogic.UserSubscribeTraceFields(&userSub)..., + )..., + ) } } else { l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type)) @@ -453,6 +474,12 @@ func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel } orderInfo.Status = orderStatusPaid l.Infow("更新订单状态成功", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("status", orderStatusPaid)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled", + "[SubscriptionFlow] apple iap attach marked order as paid", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("iap_expire_at", iapExpireAt), + )..., + ) } // enqueue activation regardless (idempotent handler downstream) payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo, IAPExpireAt: iapExpireAt} @@ -463,6 +490,12 @@ func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel l.Errorw("enqueue activate task error", logger.Field("error", err.Error())) } else { l.Infow("已加入订单激活队列", logger.Field("orderNo", orderInfo.OrderNo)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued", + "[SubscriptionFlow] apple iap attach enqueued activation task", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("iap_expire_at", iapExpireAt), + )..., + ) } return nil } diff --git a/internal/logic/public/order/purchaseLogic.go b/internal/logic/public/order/purchaseLogic.go index b41604d..df02908 100644 --- a/internal/logic/public/order/purchaseLogic.go +++ b/internal/logic/public/order/purchaseLogic.go @@ -63,6 +63,17 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P return nil, entErr } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_create_start", + "[SubscriptionFlow] purchase order creation started", + logger.Field("order_kind", "purchase"), + logger.Field("user_id", u.Id), + logger.Field("effective_user_id", entitlement.EffectiveUserID), + logger.Field("requested_subscribe_id", req.SubscribeId), + logger.Field("quantity", req.Quantity), + logger.Field("payment_id", req.Payment), + logger.Field("coupon", req.Coupon), + ) + if req.Quantity <= 0 { l.Debugf("[Purchase] Quantity is less than or equal to 0, setting to 1") req.Quantity = 1 @@ -102,12 +113,15 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P parentOrderID = decision.Anchor.OrderId subscribeToken = decision.Anchor.Token anchorUserSubscribeID = decision.Anchor.Id - l.Infow("[Purchase] single mode purchase routed to renewal", - logger.Field("mode", "single"), + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_route_selected", + "[SubscriptionFlow] purchase routed to renewal before order creation", + logger.Field("route_mode", "single"), logger.Field("route", "purchase_to_renewal"), logger.Field("anchor_user_subscribe_id", decision.Anchor.Id), - logger.Field("order_no", "pending"), logger.Field("user_id", u.Id), + logger.Field("effective_user_id", entitlement.EffectiveUserID), + logger.Field("requested_subscribe_id", req.SubscribeId), + logger.Field("resolved_subscribe_id", targetSubscribeID), ) } } @@ -126,11 +140,15 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P orderType = 2 parentOrderID = existSub.OrderId subscribeToken = existSub.Token - l.Infow("[Purchase] purchase routed to renewal/change plan (existing subscription found)", + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_route_selected", + "[SubscriptionFlow] purchase routed to renewal because an existing subscription was found", + logger.Field("route_mode", "global_single_subscription"), + logger.Field("route", "purchase_to_existing_subscription"), logger.Field("existing_subscribe_id", existSub.Id), logger.Field("existing_status", existSub.Status), logger.Field("user_id", u.Id), - logger.Field("subscribe_id", targetSubscribeID), + logger.Field("effective_user_id", entitlement.EffectiveUserID), + logger.Field("resolved_subscribe_id", targetSubscribeID), ) } } @@ -301,13 +319,13 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P AppAccountToken: uuid.New().String(), } if isSingleModeRenewal { - l.Infow("[Purchase] single mode purchase order created as renewal", - logger.Field("mode", "single"), - logger.Field("route", "purchase_to_renewal"), - logger.Field("anchor_user_subscribe_id", anchorUserSubscribeID), - logger.Field("order_no", orderInfo.OrderNo), - logger.Field("parent_id", orderInfo.ParentId), - logger.Field("user_id", u.Id), + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created", + "[SubscriptionFlow] purchase order persisted as renewal", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("route_mode", "single"), + logger.Field("route", "purchase_to_renewal"), + logger.Field("anchor_user_subscribe_id", anchorUserSubscribeID), + )..., ) } // Database transaction @@ -404,6 +422,16 @@ func (l *PurchaseLogic) Purchase(req *types.PurchaseOrderRequest) (resp *types.P } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error()) } + + if !isSingleModeRenewal { + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created", + "[SubscriptionFlow] purchase order persisted", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("route_mode", "standard"), + logger.Field("resolved_subscribe_id", targetSubscribeID), + )..., + ) + } // Deferred task payload := queue.DeferCloseOrderPayload{ OrderNo: orderInfo.OrderNo, diff --git a/internal/logic/public/order/renewalLogic.go b/internal/logic/public/order/renewalLogic.go index 1642fee..8d10023 100644 --- a/internal/logic/public/order/renewalLogic.go +++ b/internal/logic/public/order/renewalLogic.go @@ -54,6 +54,17 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene if entErr != nil { return nil, entErr } + + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_create_start", + "[SubscriptionFlow] renewal order creation started", + logger.Field("order_kind", "renewal"), + logger.Field("user_id", u.Id), + logger.Field("effective_user_id", entitlement.EffectiveUserID), + logger.Field("requested_user_subscribe_id", req.UserSubscribeID), + logger.Field("quantity", req.Quantity), + logger.Field("payment_id", req.Payment), + logger.Field("coupon", req.Coupon), + ) if req.Quantity <= 0 { l.Debugf("[Renewal] Quantity is less than or equal to 0, setting to 1") req.Quantity = 1 @@ -180,22 +191,22 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene UserId: u.Id, SubscriptionUserId: entitlement.EffectiveUserID, ParentId: userSubscribe.OrderId, - OrderNo: orderNo, - Type: 2, - Quantity: req.Quantity, - Price: price, - Amount: amount, - GiftAmount: deductionAmount, - Discount: discountAmount, - Coupon: req.Coupon, - CouponDiscount: coupon, - PaymentId: payment.Id, - Method: canonicalOrderMethod(payment.Platform), - FeeAmount: feeAmount, - Status: 1, - SubscribeId: userSubscribe.SubscribeId, - SubscribeToken: userSubscribe.Token, - AppAccountToken: uuid.New().String(), + OrderNo: orderNo, + Type: 2, + Quantity: req.Quantity, + Price: price, + Amount: amount, + GiftAmount: deductionAmount, + Discount: discountAmount, + Coupon: req.Coupon, + CouponDiscount: coupon, + PaymentId: payment.Id, + Method: canonicalOrderMethod(payment.Platform), + FeeAmount: feeAmount, + Status: 1, + SubscribeId: userSubscribe.SubscribeId, + SubscribeToken: userSubscribe.Token, + AppAccountToken: uuid.New().String(), } // Database transaction err = l.svcCtx.DB.Transaction(func(db *gorm.DB) error { @@ -235,6 +246,14 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene l.Errorw("[Renewal] Database insert error", logger.Field("error", err.Error()), logger.Field("order", orderInfo)) return nil, errors.Wrapf(err, "insert order error: %v", err.Error()) } + + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "order_created", + "[SubscriptionFlow] renewal order persisted", + append(commonLogic.OrderTraceFields(&orderInfo), + logger.Field("requested_user_subscribe_id", req.UserSubscribeID), + logger.Field("resolved_user_subscribe_id", userSubscribe.Id), + )..., + ) // Deferred task payload := queue.DeferCloseOrderPayload{ OrderNo: orderInfo.OrderNo, diff --git a/internal/logic/public/portal/purchaseCheckoutLogic.go b/internal/logic/public/portal/purchaseCheckoutLogic.go index 2370350..edc95cc 100644 --- a/internal/logic/public/portal/purchaseCheckoutLogic.go +++ b/internal/logic/public/portal/purchaseCheckoutLogic.go @@ -9,6 +9,7 @@ import ( "strings" "time" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/report" "github.com/perfect-panel/server/pkg/constant" @@ -75,6 +76,14 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest l.Logger.Error("[PurchaseCheckout] Database query error", logger.Field("error", err.Error()), logger.Field("payment", orderInfo.Method)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find payment method error: %v", err.Error()) } + + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_start", + "[SubscriptionFlow] checkout started", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", paymentConfig.Platform), + logger.Field("has_return_url", req.ReturnUrl != ""), + )..., + ) // Route to appropriate payment handler based on payment platform switch paymentPlatform.ParsePlatform(orderInfo.Method) { case paymentPlatform.AppleIAP: @@ -83,6 +92,14 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest Type: "apple_iap", ProductIds: []string{productId}, } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_response_ready", + "[SubscriptionFlow] checkout response prepared", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", paymentConfig.Platform), + logger.Field("checkout_type", resp.Type), + logger.Field("product_ids", resp.ProductIds), + )..., + ) return resp, nil case paymentPlatform.EPay: // Process EPay payment - generates payment URL for redirect @@ -157,6 +174,16 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest l.Errorw("[PurchaseCheckout] payment method not found", logger.Field("method", orderInfo.Method)) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "payment method not found") } + + if resp != nil { + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "checkout_response_ready", + "[SubscriptionFlow] checkout response prepared", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("payment_platform", paymentConfig.Platform), + logger.Field("checkout_type", resp.Type), + )..., + ) + } return } @@ -503,6 +530,9 @@ func (l *PurchaseCheckoutLogic) queryExchangeRate(to string, src int64) (amount func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) error { var userInfo user.User var err error + var giftUsed int64 + var balanceUsed int64 + paymentPath := "balance" if o.Amount == 0 { // No payment required for zero-amount orders l.Logger.Info( @@ -518,6 +548,13 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err logger.Field("userId", u.Id)) return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Update order status error: %s", err.Error()) } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled", + "[SubscriptionFlow] order marked paid without external payment", + append(commonLogic.OrderTraceFields(o), + logger.Field("payment_path", "zero_amount"), + )..., + ) + paymentPath = "zero_amount" goto activation } @@ -536,7 +573,6 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err } // Calculate payment distribution: prioritize gift amount first - var giftUsed, balanceUsed int64 remainingAmount := o.Amount if userInfo.GiftAmount >= remainingAmount { @@ -621,6 +657,15 @@ func (l *PurchaseCheckoutLogic) balancePayment(u *user.User, o *order.Order) err return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "payment_settled", + "[SubscriptionFlow] balance payment settled and order marked paid", + append(commonLogic.OrderTraceFields(o), + logger.Field("payment_path", "balance"), + logger.Field("gift_used", giftUsed), + logger.Field("balance_used", balanceUsed), + )..., + ) + activation: // Enqueue order activation task for immediate processing payload := queueType.ForthwithActivateOrderPayload{ @@ -639,6 +684,13 @@ activation: return err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "activation_task_enqueued", + "[SubscriptionFlow] activation task enqueued after checkout payment", + append(commonLogic.OrderTraceFields(o), + logger.Field("payment_path", paymentPath), + )..., + ) + l.Logger.Info("[PurchaseCheckout] Balance payment completed successfully", logger.Field("orderNo", o.OrderNo), logger.Field("userId", u.Id)) diff --git a/internal/logic/public/user/bindEmailWithVerificationLogic.go b/internal/logic/public/user/bindEmailWithVerificationLogic.go index a44c584..3abce2a 100644 --- a/internal/logic/public/user/bindEmailWithVerificationLogic.go +++ b/internal/logic/public/user/bindEmailWithVerificationLogic.go @@ -8,6 +8,7 @@ import ( "time" "github.com/perfect-panel/server/internal/config" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -43,6 +44,12 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_start", + "[SubscriptionFlow] email bind with verification started", + logger.Field("device_user_id", u.Id), + logger.Field("email", req.Email), + ) + type payload struct { Code string `json:"code"` LastAt int64 `json:"lastAt"` @@ -69,6 +76,12 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi return nil, errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code error or expired") } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_code_verified", + "[SubscriptionFlow] email verification code accepted", + logger.Field("device_user_id", u.Id), + logger.Field("email", req.Email), + ) + familyHelper := newFamilyBindingHelper(l.ctx, l.svcCtx) currentEmailMethod, err := familyHelper.getUserEmailMethod(u.Id) if err != nil { @@ -115,6 +128,13 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi return nil, txErr } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "email_owner_created", + "[SubscriptionFlow] new email owner account created for bind flow", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", emailUser.Id), + logger.Field("email", req.Email), + ) + // Join family: email user as owner, device user as member if err = familyHelper.validateJoinFamily(emailUser.Id, u.Id); err != nil { return nil, err @@ -123,11 +143,32 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi if err != nil { return nil, err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined", + "[SubscriptionFlow] device user joined email owner family", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", emailUser.Id), + logger.Field("family_id", joinResult.FamilyId), + logger.Field("email", req.Email), + ) token, err := l.refreshBindSessionToken(u.Id) if err != nil { return nil, err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_requested", + "[SubscriptionFlow] evaluating trial grant after email bind", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", emailUser.Id), + logger.Field("family_id", joinResult.FamilyId), + logger.Field("email", req.Email), + ) tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, emailUser.Id, req.Email) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_complete", + "[SubscriptionFlow] email bind with verification completed", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", emailUser.Id), + logger.Field("family_id", joinResult.FamilyId), + logger.Field("email", req.Email), + ) return &types.BindEmailWithVerificationResponse{ Success: true, Message: "email user created and joined family", @@ -146,16 +187,44 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi if err = familyHelper.validateJoinFamily(existingMethod.UserId, u.Id); err != nil { return nil, err } + + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "email_owner_resolved", + "[SubscriptionFlow] existing email owner resolved for bind flow", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", existingMethod.UserId), + logger.Field("email", req.Email), + ) joinResult, err := familyHelper.joinFamily(existingMethod.UserId, u.Id, "bind_email_with_verification") if err != nil { return nil, err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "family_joined", + "[SubscriptionFlow] device user joined existing email owner family", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", existingMethod.UserId), + logger.Field("family_id", joinResult.FamilyId), + logger.Field("email", req.Email), + ) token, err := l.refreshBindSessionToken(u.Id) if err != nil { return nil, err } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_requested", + "[SubscriptionFlow] evaluating trial grant after existing email owner bind", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", existingMethod.UserId), + logger.Field("family_id", joinResult.FamilyId), + logger.Field("email", req.Email), + ) tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, existingMethod.UserId, req.Email) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "bind_complete", + "[SubscriptionFlow] email bind with verification completed", + logger.Field("device_user_id", u.Id), + logger.Field("owner_user_id", existingMethod.UserId), + logger.Field("family_id", joinResult.FamilyId), + logger.Field("email", req.Email), + ) return &types.BindEmailWithVerificationResponse{ Success: true, diff --git a/internal/logic/public/user/emailTrialGrant.go b/internal/logic/public/user/emailTrialGrant.go index 530761b..0451ed3 100644 --- a/internal/logic/public/user/emailTrialGrant.go +++ b/internal/logic/public/user/emailTrialGrant.go @@ -5,6 +5,7 @@ import ( "time" "github.com/perfect-panel/server/internal/logic/auth" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/pkg/logger" @@ -14,18 +15,28 @@ import ( func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, log logger.Logger, ownerUserId int64, email string) { rc := svcCtx.Config.Register + commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_evaluating", + "[SubscriptionFlow] evaluating email bind trial grant", + logger.Field("owner_user_id", ownerUserId), + logger.Field("email", email), + logger.Field("trial_subscribe_id", rc.TrialSubscribe), + ) if !auth.ShouldAutoGrantTrialOnPublicEmailFlows(rc) { - log.Infow("auto trial on email flow disabled, skip", + commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped", + "[SubscriptionFlow] auto trial on public email flow disabled", logger.Field("email", email), logger.Field("owner_user_id", ownerUserId), + logger.Field("skip_reason", "public_email_trial_disabled"), ) return } if !auth.ShouldGrantTrialForEmail(rc, email) { if rc.EnableTrial && rc.EnableTrialEmailWhitelist { - log.Infow("email domain not in trial whitelist, skip", + commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped", + "[SubscriptionFlow] email domain not in trial whitelist", logger.Field("email", email), logger.Field("owner_user_id", ownerUserId), + logger.Field("skip_reason", "trial_whitelist_rejected"), ) } return @@ -36,12 +47,20 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l Model(&user.Subscribe{}). Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe). Count(&count).Error; err != nil { - log.Errorw("failed to check existing trial", logger.Field("error", err.Error())) + commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error", + "[SubscriptionFlow] failed to query existing trial subscription", + logger.Field("owner_user_id", ownerUserId), + logger.Field("email", email), + logger.Field("error", err.Error()), + ) return } if count > 0 { - log.Infow("trial already granted, skip", + commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped", + "[SubscriptionFlow] trial already exists for owner", logger.Field("owner_user_id", ownerUserId), + logger.Field("email", email), + logger.Field("skip_reason", "trial_already_exists"), ) return } @@ -49,16 +68,24 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l // Cross-user check: prevent the same real inbox (via dot trick / + alias) from // getting multiple trials across different accounts. if auth.NormalizedEmailHasTrial(ctx, svcCtx.DB, email, rc.TrialSubscribe) { - log.Infow("normalized email already has trial via another account, skip", + commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_skipped", + "[SubscriptionFlow] normalized email already received a trial elsewhere", logger.Field("email", email), logger.Field("owner_user_id", ownerUserId), + logger.Field("skip_reason", "normalized_email_has_trial"), ) return } sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe) if err != nil { - log.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error())) + commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error", + "[SubscriptionFlow] failed to load trial subscription template", + logger.Field("owner_user_id", ownerUserId), + logger.Field("email", email), + logger.Field("trial_subscribe_id", rc.TrialSubscribe), + logger.Field("error", err.Error()), + ) return } @@ -76,9 +103,13 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l Status: 1, } if err = svcCtx.UserModel.InsertSubscribe(ctx, userSub); err != nil { - log.Errorw("failed to insert trial subscribe", - logger.Field("error", err.Error()), - logger.Field("owner_user_id", ownerUserId), + commonLogic.SubscriptionTraceError(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_error", + "[SubscriptionFlow] failed to create trial subscription for email bind", + append(commonLogic.UserSubscribeTraceFields(userSub), + logger.Field("error", err.Error()), + logger.Field("owner_user_id", ownerUserId), + logger.Field("email", email), + )..., ) return } @@ -89,9 +120,12 @@ func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, l } } - log.Infow("trial granted on email bind", - logger.Field("owner_user_id", ownerUserId), - logger.Field("email", email), - logger.Field("subscribe_id", sub.Id), + commonLogic.SubscriptionTraceInfo(log, commonLogic.SubscriptionTraceFlowEmailBind, "trial_grant_succeeded", + "[SubscriptionFlow] trial subscription granted after email bind", + append(commonLogic.UserSubscribeTraceFields(userSub), + logger.Field("owner_user_id", ownerUserId), + logger.Field("email", email), + logger.Field("trial_subscribe_id", sub.Id), + )..., ) } diff --git a/internal/logic/public/user/verifyEmailLogic.go b/internal/logic/public/user/verifyEmailLogic.go index 36b46c2..c60a47f 100644 --- a/internal/logic/public/user/verifyEmailLogic.go +++ b/internal/logic/public/user/verifyEmailLogic.go @@ -8,6 +8,7 @@ import ( "time" "github.com/perfect-panel/server/internal/config" + commonLogic "github.com/perfect-panel/server/internal/logic/common" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" @@ -39,6 +40,10 @@ type CacheKeyPayload struct { func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { req.Email = strings.ToLower(strings.TrimSpace(req.Email)) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_start", + "[SubscriptionFlow] email verification started", + logger.Field("email", req.Email), + ) cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() if err != nil { @@ -59,6 +64,10 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { return errors.Wrapf(xerr.NewErrCode(xerr.VerifyCodeError), "code expired") } l.svcCtx.Redis.Del(l.ctx, cacheKey) + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_code_verified", + "[SubscriptionFlow] email verification code accepted", + logger.Field("email", req.Email), + ) u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { @@ -77,6 +86,12 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error") } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowEmailBind, "verify_email_completed", + "[SubscriptionFlow] email verification completed and trial evaluation will run", + logger.Field("user_id", u.Id), + logger.Field("owner_user_id", method.UserId), + logger.Field("email", req.Email), + ) tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, method.UserId, req.Email) return nil } diff --git a/internal/server.go b/internal/server.go index 3089e7e..2dc7a30 100644 --- a/internal/server.go +++ b/internal/server.go @@ -36,9 +36,13 @@ func NewService(svc *svc.ServiceContext) *Service { } func initServer(svc *svc.ServiceContext) *gin.Engine { - // start init system config + initStart := time.Now() + logger.Info("system initialization start") initialize.StartInitSystemConfig(svc) + logger.Infow("system initialization complete", + logger.Field("duration", time.Since(initStart).String()), + ) // init gin server r := gin.Default() r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"} diff --git a/pkg/logger/gorm.go b/pkg/logger/gorm.go index 7dbdd4a..db59ae5 100644 --- a/pkg/logger/gorm.go +++ b/pkg/logger/gorm.go @@ -9,6 +9,7 @@ import ( ) type GormLogger struct { + SlowThreshold time.Duration } const TAG = "[GORM]" @@ -27,24 +28,25 @@ func (l *GormLogger) LogMode(logger.LogLevel) logger.Interface { default: sysLevel = "unknown" } - Infof("%s System Log Level is %s", TAG, sysLevel) + Debugf("%s System Log Level is %s", TAG, sysLevel) return l } func (l *GormLogger) Info(ctx context.Context, str string, args ...interface{}) { - WithContext(ctx).WithCallerSkip(6).Infof("%s Info: %s", TAG, str, args) + WithContext(ctx).WithCallerSkip(6).Debugf("%s Info: %s", TAG, fmt.Sprintf(str, args...)) } func (l *GormLogger) Warn(ctx context.Context, str string, args ...interface{}) { - WithContext(ctx).WithCallerSkip(6).Infof("%s Warn: %s", TAG, str, args) + WithContext(ctx).WithCallerSkip(6).Debugf("%s Warn: %s", TAG, fmt.Sprintf(str, args...)) } func (l *GormLogger) Error(ctx context.Context, str string, args ...interface{}) { - WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, str, args) + WithContext(ctx).WithCallerSkip(6).Errorf("%s Error: %s", TAG, fmt.Sprintf(str, args...)) } func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { sql, rowsAffected := fc() + duration := time.Since(begin) fields := []LogField{ { Key: "sql", @@ -60,8 +62,16 @@ func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql Key: "error", Value: err.Error(), }) - WithContext(ctx).WithCallerSkip(6).WithDuration(time.Since(begin)).Errorw(TAG, fields...) - } else { - WithContext(ctx).WithCallerSkip(6).WithDuration(time.Since(begin)).Infow(fmt.Sprintf("%s SQL Executed", TAG), fields...) + WithContext(ctx).WithCallerSkip(6).WithDuration(duration).Errorw(TAG, fields...) + return + } + + if l.SlowThreshold > 0 && duration >= l.SlowThreshold { + WithContext(ctx).WithCallerSkip(6).WithDuration(duration).Sloww(fmt.Sprintf("%s SQL Slow", TAG), fields...) + return + } + + if shallLog(DebugLevel) { + WithContext(ctx).WithCallerSkip(6).WithDuration(duration).Debugw(fmt.Sprintf("%s SQL Executed", TAG), fields...) } } diff --git a/pkg/orm/mysql.go b/pkg/orm/mysql.go index 39ce1e0..5cddd6d 100644 --- a/pkg/orm/mysql.go +++ b/pkg/orm/mysql.go @@ -46,7 +46,9 @@ func ConnectMysql(m Mysql) (*gorm.DB, error) { DSN: m.Dsn(), } db, err := gorm.Open(mysql.New(mysqlCfg), &gorm.Config{ - Logger: new(logger.GormLogger), + Logger: &logger.GormLogger{ + SlowThreshold: m.GetSlowThreshold(), + }, NamingStrategy: schema.NamingStrategy{ SingularTable: true, }, diff --git a/queue/logic/order/activateOrderLogic.go b/queue/logic/order/activateOrderLogic.go index 4577e8e..f1f5222 100644 --- a/queue/logic/order/activateOrderLogic.go +++ b/queue/logic/order/activateOrderLogic.go @@ -28,6 +28,7 @@ import ( "github.com/perfect-panel/server/pkg/uuidx" queueTypes "github.com/perfect-panel/server/queue/types" "gorm.io/gorm" + "gorm.io/gorm/clause" ) // Order type constants define the different types of orders that can be processed @@ -71,8 +72,10 @@ func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic { // It handles the complete workflow of activating a paid order including validation, // processing based on order type, and finalization. func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { - logger.WithContext(ctx).Info("[ActivateOrderLogic] 开始处理订单激活任务", - logger.Field("payload", string(task.Payload()))) + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_task_received", + "[SubscriptionFlow] activation task received", + logger.Field("payload", string(task.Payload())), + ) payload, err := l.parsePayload(ctx, task.Payload()) if err != nil { @@ -81,8 +84,11 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) return nil // payload 解析失败不重试,因为重试也会失败 } - logger.WithContext(ctx).Info("[ActivateOrderLogic] 正在验证订单", - logger.Field("order_no", payload.OrderNo)) + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_order_lookup", + "[SubscriptionFlow] activation task is loading order", + logger.Field("order_no", payload.OrderNo), + logger.Field("iap_expire_at", payload.IAPExpireAt), + ) orderInfo, err := l.claimAndGetOrder(ctx, payload.OrderNo) if err != nil { @@ -104,10 +110,10 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) return nil } - logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单验证通过,开始处理", - logger.Field("order_no", orderInfo.OrderNo), - logger.Field("order_type", orderInfo.Type), - logger.Field("user_id", orderInfo.UserId)) + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_order_claimed", + "[SubscriptionFlow] activation worker claimed paid order", + commonLogic.OrderTraceFields(orderInfo)..., + ) if err = l.processOrderByType(ctx, orderInfo, payload.IAPExpireAt); err != nil { l.releaseClaim(ctx, orderInfo.OrderNo) @@ -118,12 +124,21 @@ func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) return err // 返回 err 允许 asynq 重试 } + if err = l.reconcilePostOrderSubscriptions(ctx, orderInfo); err != nil { + l.releaseClaim(ctx, orderInfo.OrderNo) + logger.WithContext(ctx).Error("[ActivateOrderLogic] 订单订阅兜底合并失败,将重试", + logger.Field("order_no", orderInfo.OrderNo), + logger.Field("order_type", orderInfo.Type), + logger.Field("error", err.Error())) + return err + } + l.finalizeCouponAndOrder(ctx, orderInfo) - logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单激活成功", - logger.Field("order_no", orderInfo.OrderNo), - logger.Field("order_type", orderInfo.Type), - logger.Field("user_id", orderInfo.UserId)) + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_finished", + "[SubscriptionFlow] order activation completed", + commonLogic.OrderTraceFields(orderInfo)..., + ) return nil } @@ -217,6 +232,311 @@ func (l *ActivateOrderLogic) processOrderByType(ctx context.Context, orderInfo * } } +func (l *ActivateOrderLogic) reconcilePostOrderSubscriptions(ctx context.Context, orderInfo *order.Order) error { + if !shouldReconcilePostOrderSubscriptions(orderInfo) { + return nil + } + + effectiveUserID := orderInfo.UserId + if orderInfo.SubscriptionUserId > 0 { + effectiveUserID = orderInfo.SubscriptionUserId + } + if effectiveUserID == 0 || orderInfo.Id == 0 { + return nil + } + + var ( + survivor user.Subscribe + survivorBefore user.Subscribe + losers []user.Subscribe + mergedIDs []int64 + subscribeIDsToClear = make(map[int64]struct{}) + missingSurvivor bool + ownerMismatchSkipped bool + identitySourceID int64 + ) + + err := l.svc.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&user.Subscribe{}). + Where("order_id = ?", orderInfo.Id). + First(&survivor).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + missingSurvivor = true + return nil + } + return err + } + survivorBefore = survivor + + var ownerSubs []user.Subscribe + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&user.Subscribe{}). + Where("user_id = ?", effectiveUserID). + Order("id ASC"). + Find(&ownerSubs).Error; err != nil { + return err + } + + if survivor.UserId != effectiveUserID { + if len(ownerSubs) == 0 { + ownerMismatchSkipped = true + return nil + } + + if err := tx.Model(&user.Subscribe{}). + Where("id = ?", survivor.Id). + Update("user_id", effectiveUserID).Error; err != nil { + return err + } + survivor.UserId = effectiveUserID + + if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). + Model(&user.Subscribe{}). + Where("user_id = ?", effectiveUserID). + Order("id ASC"). + Find(&ownerSubs).Error; err != nil { + return err + } + } + + if len(ownerSubs) <= 1 { + return nil + } + + maxExpire := survivor.ExpireTime + for i := range ownerSubs { + item := ownerSubs[i] + if item.Id == survivor.Id { + if item.ExpireTime.After(maxExpire) { + maxExpire = item.ExpireTime + } + continue + } + + losers = append(losers, item) + mergedIDs = append(mergedIDs, item.Id) + if item.ExpireTime.After(maxExpire) { + maxExpire = item.ExpireTime + } + if item.SubscribeId > 0 { + subscribeIDsToClear[item.SubscribeId] = struct{}{} + } + } + + if len(losers) == 0 { + return nil + } + + if survivor.SubscribeId > 0 { + subscribeIDsToClear[survivor.SubscribeId] = struct{}{} + } + + identitySource := pickSubscriptionIdentitySource(losers) + if identitySource != nil { + identitySourceID = identitySource.Id + } + + updateFields := map[string]interface{}{ + "status": 1, + "finished_at": nil, + } + if maxExpire.After(survivor.ExpireTime) { + survivor.ExpireTime = maxExpire + updateFields["expire_time"] = maxExpire + } + if identitySource != nil { + if identitySource.Token != "" { + survivor.Token = identitySource.Token + updateFields["token"] = identitySource.Token + } + if identitySource.UUID != "" { + survivor.UUID = identitySource.UUID + updateFields["uuid"] = identitySource.UUID + } + } + + loserIDs := make([]int64, 0, len(losers)) + for i := range losers { + loserIDs = append(loserIDs, losers[i].Id) + } + if len(loserIDs) == 0 { + return nil + } + + // user_subscribe 当前没有 deleted_at 字段,这里沿用项目现有删除语义清理 loser 记录。 + if err := tx.Where("id IN ?", loserIDs).Delete(&user.Subscribe{}).Error; err != nil { + return err + } + + if err := tx.Model(&user.Subscribe{}). + Where("id = ?", survivor.Id). + Updates(updateFields).Error; err != nil { + return err + } + survivor.Status = 1 + survivor.FinishedAt = nil + + return nil + }) + if err != nil { + return err + } + + if missingSurvivor { + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "post_order_reconcile_skipped", + "[SubscriptionFlow] post-order reconcile skipped because survivor subscription was not found", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("reason", "post_order_reconcile"), + logger.Field("effective_user_id", effectiveUserID), + )..., + ) + return nil + } + + if ownerMismatchSkipped { + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "post_order_reconcile_skipped", + "[SubscriptionFlow] post-order reconcile skipped because survivor owner mismatch had no duplicates", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("reason", "post_order_reconcile"), + logger.Field("effective_user_id", effectiveUserID), + logger.Field("survivor_subscribe_id", survivor.Id), + logger.Field("survivor_user_id", survivorBefore.UserId), + )..., + ) + return nil + } + + if len(losers) == 0 { + return nil + } + + l.clearPostOrderReconcileCache(ctx, &survivorBefore, &survivor, losers, subscribeIDsToClear) + + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "post_order_reconciled", + "[SubscriptionFlow] post-order reconcile merged duplicate subscriptions", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("reason", "post_order_reconcile"), + logger.Field("effective_user_id", effectiveUserID), + logger.Field("survivor_subscribe_id", survivor.Id), + logger.Field("identity_source_subscribe_id", identitySourceID), + logger.Field("merged_subscribe_ids", mergedIDs), + logger.Field("merged_count", len(mergedIDs)), + )..., + ) + + return nil +} + +func shouldReconcilePostOrderSubscriptions(orderInfo *order.Order) bool { + if orderInfo == nil { + return false + } + + switch orderInfo.Type { + case OrderTypeSubscribe, OrderTypeRenewal, OrderTypeRedemption: + return true + default: + return false + } +} + +func pickSubscriptionIdentitySource(candidates []user.Subscribe) *user.Subscribe { + if len(candidates) == 0 { + return nil + } + + best := &candidates[0] + for i := 1; i < len(candidates); i++ { + candidate := &candidates[i] + if subscriptionIdentityPriority(candidate, best) { + best = candidate + } + } + return best +} + +func subscriptionIdentityPriority(candidate *user.Subscribe, current *user.Subscribe) bool { + if candidate == nil { + return false + } + if current == nil { + return true + } + + candidateUsable := candidate.Token != "" || candidate.UUID != "" + currentUsable := current.Token != "" || current.UUID != "" + if candidateUsable != currentUsable { + return candidateUsable + } + + if candidate.ExpireTime.After(current.ExpireTime) { + return true + } + if current.ExpireTime.After(candidate.ExpireTime) { + return false + } + + if candidate.UpdatedAt.After(current.UpdatedAt) { + return true + } + if current.UpdatedAt.After(candidate.UpdatedAt) { + return false + } + + return candidate.Id > current.Id +} + +func (l *ActivateOrderLogic) clearPostOrderReconcileCache( + ctx context.Context, + survivorBefore *user.Subscribe, + survivorAfter *user.Subscribe, + losers []user.Subscribe, + subscribeIDs map[int64]struct{}, +) { + cacheModels := make([]*user.Subscribe, 0, len(losers)+2) + if survivorBefore != nil { + cacheModels = append(cacheModels, survivorBefore) + } + if survivorAfter != nil { + cacheModels = append(cacheModels, survivorAfter) + } + for i := range losers { + loser := losers[i] + cacheModels = append(cacheModels, &loser) + } + + if len(cacheModels) > 0 { + if err := l.svc.UserModel.ClearSubscribeCache(ctx, cacheModels...); err != nil { + logger.WithContext(ctx).Error("Post-order reconcile clear subscribe cache failed", + logger.Field("reason", "post_order_reconcile"), + logger.Field("error", err.Error()), + ) + } + } + + if l.svc.SubscribeModel != nil { + for subscribeID := range subscribeIDs { + if err := l.svc.SubscribeModel.ClearCache(ctx, subscribeID); err != nil { + logger.WithContext(ctx).Error("Post-order reconcile clear plan cache failed", + logger.Field("reason", "post_order_reconcile"), + logger.Field("subscribe_id", subscribeID), + logger.Field("error", err.Error()), + ) + } + } + } + + if l.svc.NodeModel != nil { + if err := l.svc.NodeModel.ClearServerAllCache(ctx); err != nil { + logger.WithContext(ctx).Error("Post-order reconcile clear node cache failed", + logger.Field("reason", "post_order_reconcile"), + logger.Field("error", err.Error()), + ) + } + } +} + // finalizeCouponAndOrder handles post-processing tasks including coupon updates // and order status finalization func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderInfo *order.Order) { @@ -238,6 +558,10 @@ func (l *ActivateOrderLogic) finalizeCouponAndOrder(ctx context.Context, orderIn ) } orderInfo.Status = OrderStatusFinished + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "order_status_finished", + "[SubscriptionFlow] order status updated to finished", + commonLogic.OrderTraceFields(orderInfo)..., + ) } // NewPurchase handles new subscription purchase including user creation, @@ -248,6 +572,13 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O return err } + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "activation_user_resolved", + "[SubscriptionFlow] activation resolved subscription recipient user", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("resolved_user_id", userInfo.Id), + )..., + ) + sub, err := l.getSubscribeInfo(ctx, orderInfo.SubscribeId) if err != nil { return err @@ -288,12 +619,14 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O ) } else { userSub = anchorSub - logger.WithContext(ctx).Infow("Single mode purchase routed to renewal in activation", - logger.Field("mode", "single"), - logger.Field("route", "purchase_to_renewal"), - logger.Field("plan_changed", anchorSub.SubscribeId != orderInfo.SubscribeId), - logger.Field("anchor_user_subscribe_id", anchorSub.Id), - logger.Field("order_no", orderInfo.OrderNo), + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_reused", + "[SubscriptionFlow] activation reused single-mode anchor subscription", + append(commonLogic.OrderTraceFields(orderInfo), + append(commonLogic.UserSubscribeTraceFields(anchorSub), + logger.Field("reuse_reason", "single_mode_purchase_to_renewal"), + logger.Field("plan_changed", anchorSub.SubscribeId != orderInfo.SubscribeId), + )..., + )..., ) } case errors.Is(anchorErr, gorm.ErrRecordNotFound): @@ -359,11 +692,15 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O ) } else { userSub = &existingSub - logger.WithContext(ctx).Infow("Fallback: renewed existing subscription instead of creating duplicate", - logger.Field("existing_subscribe_id", existingSub.Id), - logger.Field("order_no", orderInfo.OrderNo), - logger.Field("candidate_user_ids", candidateUserIds), - logger.Field("owner_corrected_to", effectiveOwner), + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_reused", + "[SubscriptionFlow] activation renewed an existing subscription instead of creating a duplicate", + append(commonLogic.OrderTraceFields(orderInfo), + append(commonLogic.UserSubscribeTraceFields(&existingSub), + logger.Field("reuse_reason", "fallback_existing_subscription"), + logger.Field("candidate_user_ids", candidateUserIds), + logger.Field("owner_corrected_to", effectiveOwner), + )..., + )..., ) } } @@ -386,7 +723,12 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O // Clear cache l.clearServerCache(ctx, sub) - logger.WithContext(ctx).Info("Insert user subscribe success") + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_issued", + "[SubscriptionFlow] activation finished issuing subscription entitlement", + append(commonLogic.OrderTraceFields(orderInfo), + commonLogic.UserSubscribeTraceFields(userSub)..., + )..., + ) return nil } @@ -457,6 +799,14 @@ func (l *ActivateOrderLogic) createGuestUser(ctx context.Context, orderInfo *ord logger.Field("identifier", tempOrder.Identifier), logger.Field("auth_type", tempOrder.AuthType), ) + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "guest_user_created", + "[SubscriptionFlow] guest user created during order activation", + append(commonLogic.OrderTraceFields(orderInfo), + logger.Field("created_user_id", userInfo.Id), + logger.Field("identifier", tempOrder.Identifier), + logger.Field("auth_type", tempOrder.AuthType), + )..., + ) return userInfo, nil } @@ -570,6 +920,13 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn return nil, err } + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_created", + "[SubscriptionFlow] new user subscription record created", + append(commonLogic.OrderTraceFields(orderInfo), + commonLogic.UserSubscribeTraceFields(userSub)..., + )..., + ) + return userSub, nil } @@ -622,11 +979,15 @@ func (l *ActivateOrderLogic) extendGiftSubscription(ctx context.Context, giftSub return nil, err } - logger.WithContext(ctx).Info("Extended gift subscription successfully", - logger.Field("subscribe_id", giftSub.Id), - logger.Field("old_expire_time", baseTime), - logger.Field("new_expire_time", newExpireTime), - logger.Field("order_id", orderInfo.Id), + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_reused", + "[SubscriptionFlow] paid order extended an existing gift subscription", + append(commonLogic.OrderTraceFields(orderInfo), + append(commonLogic.UserSubscribeTraceFields(giftSub), + logger.Field("reuse_reason", "gift_subscription_promoted"), + logger.Field("old_expire_time", baseTime), + logger.Field("new_expire_time", newExpireTime), + )..., + )..., ) return giftSub, nil @@ -997,6 +1358,15 @@ func (l *ActivateOrderLogic) Renewal(ctx context.Context, orderInfo *order.Order // Handle commission go l.handleCommission(context.Background(), userInfo, orderInfo) + commonLogic.SubscriptionTraceInfo(logger.WithContext(ctx), commonLogic.SubscriptionTraceFlowOrder, "subscription_renewed", + "[SubscriptionFlow] renewal order updated existing subscription", + append(commonLogic.OrderTraceFields(orderInfo), + append(commonLogic.UserSubscribeTraceFields(userSub), + logger.Field("iap_expire_at", iapExpireAt), + )..., + )..., + ) + return nil } diff --git a/scripts/debug_device_login.go b/scripts/debug_device_login.go new file mode 100644 index 0000000..1beaa81 --- /dev/null +++ b/scripts/debug_device_login.go @@ -0,0 +1,185 @@ +//go:build ignore + +package main + +import ( + "bytes" + "crypto/md5" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "time" + + "github.com/forgoer/openssl" +) + +// ===== AES 加解密(与 pkg/aes/aes.go 一致)===== + +func generateKey(key string) []byte { + hash := sha256.Sum256([]byte(key)) + return hash[:32] +} + +func generateIv(iv, key string) []byte { + h := md5.New() + h.Write([]byte(iv)) + return generateKey(hex.EncodeToString(h.Sum(nil)) + key) +} + +func aesEncrypt(plainText []byte, keyStr string) (string, string, error) { + nonce := fmt.Sprintf("%x", time.Now().UnixNano()) + key := generateKey(keyStr) + iv := generateIv(nonce, keyStr) + dst, err := openssl.AesCBCEncrypt(plainText, key, iv, openssl.PKCS7_PADDING) + if err != nil { + return "", "", err + } + return base64.StdEncoding.EncodeToString(dst), nonce, nil +} + +func aesDecrypt(cipherText, keyStr, ivStr string) (string, error) { + decode, err := base64.StdEncoding.DecodeString(cipherText) + if err != nil { + return "", err + } + key := generateKey(keyStr) + iv := generateIv(ivStr, keyStr) + dst, err := openssl.AesCBCDecrypt(decode, key, iv, openssl.PKCS7_PADDING) + return string(dst), err +} + +// ===== 主逻辑 ===== + +func main() { + deviceID := flag.String("id", "", "设备 ID (identifier)") + secret := flag.String("secret", "", "security_secret (device.security_secret)") + host := flag.String("host", "https://api.hifast.biz", "API 地址") + flag.Parse() + + if *deviceID == "" || *secret == "" { + fmt.Println("用法: go run scripts/debug_device_login.go -id <设备ID> -secret ") + return + } + + // 1. 构造登录请求体 + loginBody := map[string]interface{}{ + "identifier": *deviceID, + "user_agent": "DebugScript/1.0", + } + loginJSON, _ := json.Marshal(loginBody) + + // 2. AES 加密请求体 + encData, nonce, err := aesEncrypt(loginJSON, *secret) + if err != nil { + fmt.Printf("❌ 加密失败: %v\n", err) + return + } + + encBody := map[string]interface{}{ + "data": encData, + "time": nonce, + } + encBodyJSON, _ := json.Marshal(encBody) + + fmt.Printf("📤 登录请求体(加密): %s\n\n", encBodyJSON) + + // 3. 发起设备登录请求 + loginURL := *host + "/v1/auth/login/device" + req, _ := http.NewRequest("POST", loginURL, bytes.NewReader(encBodyJSON)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Login-Type", "device") + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("❌ 登录请求失败: %v\n", err) + return + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + fmt.Printf("📥 登录响应(原始): %s\n\n", respBody) + + // 4. 解密响应 + var respMap map[string]interface{} + if err := json.Unmarshal(respBody, &respMap); err != nil { + fmt.Printf("❌ 解析响应 JSON 失败: %v\n", err) + return + } + + var token string + if dataField, ok := respMap["data"]; ok { + switch d := dataField.(type) { + case map[string]interface{}: + // 加密响应 + encResp, _ := d["data"].(string) + ivResp, _ := d["time"].(string) + if encResp != "" && ivResp != "" { + decrypted, err := aesDecrypt(encResp, *secret, ivResp) + if err != nil { + fmt.Printf("❌ 解密响应失败: %v\n", err) + return + } + fmt.Printf("📥 登录响应(解密): %s\n\n", decrypted) + var loginData map[string]interface{} + if err := json.Unmarshal([]byte(decrypted), &loginData); err == nil { + token, _ = loginData["token"].(string) + } + } + case string: + // 未加密直接是 token 字符串 + token = d + } + } + + if token == "" { + fmt.Println("❌ 未获取到 token,登录失败") + return + } + fmt.Printf("✅ Token: %s\n\n", token) + + // 5. 查询订阅 + subURL := *host + "/v1/public/user/subscribe" + subReq, _ := http.NewRequest("GET", subURL, nil) + subReq.Header.Set("Authorization", "Bearer "+token) + subReq.Header.Set("Login-Type", "device") + subReq.Header.Set("X-App-Id", "debug") + + subResp, err := client.Do(subReq) + if err != nil { + fmt.Printf("❌ 查询订阅失败: %v\n", err) + return + } + defer subResp.Body.Close() + + subBody, _ := io.ReadAll(subResp.Body) + fmt.Printf("📥 订阅响应(原始): %s\n\n", subBody) + + // 6. 解密订阅响应 + var subRespMap map[string]interface{} + if err := json.Unmarshal(subBody, &subRespMap); err == nil { + if dataField, ok := subRespMap["data"]; ok { + if d, ok := dataField.(map[string]interface{}); ok { + encResp, _ := d["data"].(string) + ivResp, _ := d["time"].(string) + if encResp != "" && ivResp != "" { + decrypted, err := aesDecrypt(encResp, *secret, ivResp) + if err != nil { + fmt.Printf("❌ 解密订阅响应失败: %v\n", err) + return + } + // 格式化输出 + var pretty interface{} + json.Unmarshal([]byte(decrypted), &pretty) + out, _ := json.MarshalIndent(pretty, "", " ") + fmt.Printf("📋 订阅信息(解密):\n%s\n", out) + } + } + } + } +} diff --git a/订单日子.txt b/订单日子.txt index b2a9412..2392a4f 100644 --- a/订单日子.txt +++ b/订单日子.txt @@ -1,23 +1,1140 @@ -TRUNCATE TABLE apple_iap_transactions; -TRUNCATE TABLE application_versions; -TRUNCATE TABLE `order`; -TRUNCATE TABLE system_logs; -TRUNCATE TABLE traffic_log; -TRUNCATE TABLE user; -TRUNCATE TABLE user_auth_methods; -TRUNCATE TABLE user_device; -TRUNCATE TABLE user_device_online_record; -TRUNCATE TABLE user_family; -TRUNCATE TABLE user_family_member; -TRUNCATE TABLE user_subscribe; +| US1 | 205.198.68.35
64.118.152.42 | +| ---- | -------------------------------- | +| JP1 | 205.198.79.187
64.118.144.142 | +| HK1 | 205.198.65.239
64.118.128.84 | +| SG1 | 205.198.72.111
64.118.136.4 | +| US3 | 146.19.116.100 | +| JP2 | 45.143.233.236 | +| DE1 | 45.147.48.172 | +| TW1 | 210.79.155.116 | +| HK10 | 151.243.229.150 | +| HK11 | 167.148.203.71 | +| HK12 | 167.148.203.107 | +| HK13 | 151.243.229.62 | +| HK14 | 140.150.236.143 | +| HK15 | 140.150.236.9 | +| UK1 | 82.26.82.171 | +| SG3 | 77.93.90.94 | +| US7 | 167.253.97.183 | +| HK16 | 185.241.42.105 | +| TW2 | tw-ty-line-11-1-2.sudatech.store | +| HK17 | 205.198.84.66
64.118.132.129 | +| US8 | 205.198.69.164
64.118.152.41 | +| SG4 | 205.198.86.114
64.118.140.32 | +| JP5 | 205.198.88.243
64.118.148.218 | +| FR1 | 64.118.157.103 | +| JP6 | 154.31.113.136 | +| HK18 | 154.3.39.160 | +| US9 | 216.234.142.116 | +| JP7 | 162.141.130.46 | +| TW3 | 1.170.218.105 | +| MO1 | 45.207.35.67 | +| IT1 | 45.196.215.240 | -docker exec ppanel-mysql mysql -uroot -p hifast -e " - SELECT id, referer_id, refer_code, created_at - FROM user - WHERE refer_code = 'uuD58Gs9' - ORDER BY id DESC - LIMIT 10; - " - docker logs ppanel-server 2>&1 | grep "1519\|bind_invite\|BindInviteCode" | tail -20 \ No newline at end of file + + +-----------一配置的 +[ + { + "uuid": "3da33541-5558-4566-a014-15ce26f7a587", + "token": "bu-SauFsv3RXaaAy", + "name": "US1", + "cpu_name": "AMD EPYC 7663 56-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.152.42", + "region": "🇺🇸", + "mem_total": 1945264128, + "swap_total": 0, + "disk_total": 10485227520, + "version": "1.1.38", + "weight": 6, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-05-18T00:00:00+08:00", + "group": "nube", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "sum", + "created_at": "2025-09-17T08:46:16+08:00", + "updated_at": "2026-04-21T11:04:43+08:00" + }, + { + "uuid": "fe9034af-70ee-4af9-bc75-2c43f169de29", + "token": "yME7FkssEyxN2u-z", + "name": "ZX6", + "cpu_name": "Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 8, + "os": "Debian GNU/Linux 12 (bookworm)", + "kernel_version": "6.1.0-43-amd64", + "gpu_name": "None", + "ipv4": "89.185.25.12", + "region": "🇭🇰", + "mem_total": 4103806976, + "swap_total": 0, + "disk_total": 21051832320, + "version": "1.1.93", + "weight": 5, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-05-01T00:00:00+08:00", + "group": "xiao", + "tags": "", + "hidden": false, + "traffic_limit": 108851651149824, + "traffic_limit_type": "max", + "created_at": "2025-09-17T13:16:42+08:00", + "updated_at": "2026-04-21T11:00:13+08:00" + }, + { + "uuid": "0d66b18f-89a1-4068-914f-dc0991901a6d", + "token": "o8dwT4W1bgM7o3AW", + "name": "HK1", + "cpu_name": "AMD EPYC 7713 64-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "Red Hat, Inc. Virtio 1.0 GPU (rev 01)", + "ipv4": "64.118.128.84", + "region": "🇭🇰", + "mem_total": 1945210880, + "swap_total": 0, + "disk_total": 21054046208, + "version": "1.0.83", + "weight": 8, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-05-15T00:00:00+08:00", + "group": "nube", + "tags": "", + "hidden": false, + "traffic_limit": 108851651149824, + "traffic_limit_type": "max", + "created_at": "2025-09-17T13:35:11+08:00", + "updated_at": "2026-04-21T11:08:13+08:00" + }, + { + "uuid": "18b6238d-d1cd-460e-ae8a-fcbad72abfdb", + "token": "PA1PW7W0pEFT96-U", + "name": "SG1", + "cpu_name": "AMD EPYC 7663 56-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "1111 (rev 02)", + "ipv4": "64.118.136.4", + "region": "🇸🇬", + "mem_total": 1945206784, + "swap_total": 0, + "disk_total": 10485227520, + "version": "1.0.83", + "weight": 9, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-05-15T00:00:00+08:00", + "group": "nube", + "tags": "", + "hidden": false, + "traffic_limit": 108851651149824, + "traffic_limit_type": "max", + "created_at": "2025-09-17T13:35:58+08:00", + "updated_at": "2026-04-21T11:07:37+08:00" + }, + { + "uuid": "20ff445f-f51b-4e17-9bcd-9596fa075e6e", + "token": "N-OCFldZ908g6obr", + "name": "DE1", + "cpu_name": "Intel(R) Xeon(R) Gold 6133 CPU @ 2.50GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Debian GNU/Linux 13 (trixie)", + "kernel_version": "6.12.63+deb13-cloud-amd64", + "gpu_name": "None", + "ipv4": "45.147.48.172", + "ipv6": "2a09:0:9::1:5", + "region": "🇩🇪", + "mem_total": 1014644736, + "swap_total": 268431360, + "disk_total": 21091778560, + "version": "1.1.93", + "weight": 12, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-05-01T00:00:00+08:00", + "group": "vps", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "max", + "created_at": "2025-09-17T13:36:58+08:00", + "updated_at": "2026-04-21T11:04:34+08:00" + }, + { + "uuid": "1c601df8-2563-42a3-b56d-0a87ccfc434f", + "token": "MaQ1j7ooK2uG8Zb3", + "name": "JP2", + "cpu_name": "Intel(R) Xeon(R) Platinum 8163 CPU @ 2.50GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Debian GNU/Linux 13 (trixie)", + "kernel_version": "6.12.41+deb13-cloud-amd64", + "gpu_name": "None", + "ipv4": "45.143.233.236", + "ipv6": "2a12:a301:2004::10bc", + "region": "🇯🇵", + "mem_total": 1014661120, + "swap_total": 268431360, + "disk_total": 21091778560, + "version": "1.1.93", + "weight": 11, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-04-27T00:00:00+08:00", + "group": "vps", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "max", + "created_at": "2025-09-17T13:37:17+08:00", + "updated_at": "2026-04-21T11:05:08+08:00" + }, + { + "uuid": "5d126203-986c-4c07-b11d-af153317abc9", + "token": "lujYVrol797YARwk", + "name": "US3", + "cpu_name": "Intel(R) Xeon(R) Gold 6133 CPU @ 2.50GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.3 LTS", + "kernel_version": "6.8.0-90-generic", + "gpu_name": "None", + "ipv4": "146.19.116.100", + "ipv6": "2604:a840:2::6ba", + "region": "🇺🇸", + "mem_total": 1007820800, + "swap_total": 268431360, + "disk_total": 21091778560, + "version": "1.0.83", + "weight": 10, + "price": -1, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-05-21T00:00:00+08:00", + "group": "", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "max", + "created_at": "2025-09-17T13:39:09+08:00", + "updated_at": "2026-04-21T11:06:09+08:00" + }, + { + "uuid": "2c56b3ff-3f58-404c-836e-f3741c97c87b", + "token": "HUAdYT015_GADDYk", + "name": "HK10", + "cpu_name": "AMD Ryzen 9 9950X 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 22.04 LTS", + "kernel_version": "5.15.0-164-generic", + "gpu_name": "Cirrus Logic GD 5446", + "ipv4": "151.243.229.150", + "ipv6": "2a0e:97c0:3f0:1::156c", + "region": "🇭🇰", + "mem_total": 2058956800, + "swap_total": 0, + "disk_total": 15525066752, + "version": "1.0.81", + "weight": 19, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-09-21T00:00:00+08:00", + "group": "zout", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "max", + "created_at": "2025-09-22T04:03:48+08:00", + "updated_at": "2026-04-21T11:04:51+08:00" + }, + { + "uuid": "f3c0e1af-0d1f-4e9e-93ea-ab396e7a8566", + "token": "5fJ9ft6leSkY5K2l", + "name": "HK11", + "cpu_name": "AMD Ryzen 9 9950X 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 22.04 LTS", + "kernel_version": "5.15.0-168-generic", + "gpu_name": "Cirrus Logic GD 5446", + "ipv4": "167.148.203.71", + "ipv6": "2a0e:97c0:3f0:1::156b", + "region": "🇭🇰", + "mem_total": 2058960896, + "swap_total": 0, + "disk_total": 15525066752, + "version": "1.0.81", + "weight": 20, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-09-21T00:00:00+08:00", + "group": "zout", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "max", + "created_at": "2025-09-22T04:03:55+08:00", + "updated_at": "2026-04-21T11:07:27+08:00" + }, + { + "uuid": "db37dd06-2bec-46a1-91d4-abff795b254b", + "token": "QVs8_aF6jPRjcBf6", + "name": "HK12", + "cpu_name": "AMD Ryzen 9 9950X 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 22.04 LTS", + "kernel_version": "5.15.0-164-generic", + "gpu_name": "Cirrus Logic GD 5446", + "ipv4": "167.148.203.107", + "ipv6": "2a0e:97c0:3f0:1::156a", + "region": "🇭🇰", + "mem_total": 2058960896, + "swap_total": 0, + "disk_total": 15525066752, + "version": "1.0.81", + "weight": 21, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-09-21T00:00:00+08:00", + "group": "zout", + "tags": "", + "hidden": false, + "traffic_limit": 1099511627776, + "traffic_limit_type": "max", + "created_at": "2025-09-22T04:04:00+08:00", + "updated_at": "2026-04-21T11:04:41+08:00" + }, + { + "uuid": "3f7d0e86-4656-4685-b2ed-116d8fb70cf2", + "token": "DXWFn1D9_MSZWg-5", + "name": "HK13", + "cpu_name": "Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 22.04 LTS", + "kernel_version": "5.15.0-164-generic", + "gpu_name": "Cirrus Logic GD 5446", + "ipv4": "151.243.229.62", + "region": "🇭🇰", + "mem_total": 2058960896, + "swap_total": 0, + "disk_total": 15525066752, + "version": "1.0.83", + "weight": 22, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-01T00:00:00+08:00", + "group": "zout", + "tags": "", + "hidden": false, + "traffic_limit": 2199023255552, + "traffic_limit_type": "max", + "created_at": "2025-10-02T03:01:47+08:00", + "updated_at": "2026-04-21T11:06:55+08:00" + }, + { + "uuid": "96f98782-0cb3-4932-87b7-e63095d45192", + "token": "mUS6W-QX3Qwlb4Lk", + "name": "HK14", + "cpu_name": "Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 22.04 LTS", + "kernel_version": "5.15.0-164-generic", + "gpu_name": "Cirrus Logic GD 5446", + "ipv4": "140.150.236.143", + "region": "🇭🇰", + "mem_total": 2058960896, + "swap_total": 0, + "disk_total": 15525066752, + "version": "1.0.83", + "weight": 23, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-01T00:00:00+08:00", + "group": "zout", + "tags": "", + "hidden": false, + "traffic_limit": 2199023255552, + "traffic_limit_type": "max", + "created_at": "2025-10-02T03:01:52+08:00", + "updated_at": "2026-04-21T11:07:24+08:00" + }, + { + "uuid": "63d2560e-266c-4cd8-b02f-374e2edc1cbf", + "token": "BPdWwljwvVkh-MCt", + "name": "HK15", + "cpu_name": "Intel(R) Xeon(R) Platinum 8269CY CPU @ 2.50GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 22.04 LTS", + "kernel_version": "5.15.0-164-generic", + "gpu_name": "Cirrus Logic GD 5446", + "ipv4": "140.150.236.9", + "region": "🇭🇰", + "mem_total": 2058960896, + "swap_total": 0, + "disk_total": 15525066752, + "version": "1.0.83", + "weight": 24, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-01T00:00:00+08:00", + "group": "zout", + "tags": "", + "hidden": false, + "traffic_limit": 2199023255552, + "traffic_limit_type": "max", + "created_at": "2025-10-02T03:01:58+08:00", + "updated_at": "2026-04-21T11:05:54+08:00" + }, + { + "uuid": "79960af7-7ccc-4921-9cbb-0fed6f0b6402", + "token": "8NJNW3Pf-4bQTqIN", + "name": "UK1", + "cpu_name": "AMD Ryzen 9 9950X 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 1, + "os": "Ubuntu 24.10", + "kernel_version": "6.11.0-13-generic", + "gpu_name": "None", + "ipv4": "82.26.82.171", + "ipv6": "2a12:bec4:1873:4a4::f2d", + "region": "🇬🇧", + "mem_total": 1727528960, + "swap_total": 0, + "disk_total": 31614631936, + "version": "1.1.93", + "weight": 25, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-02T00:00:00+08:00", + "group": "", + "tags": "", + "hidden": false, + "traffic_limit": 8796093022208, + "traffic_limit_type": "max", + "created_at": "2025-10-05T15:28:40+08:00", + "updated_at": "2026-04-21T11:05:43+08:00" + }, + { + "uuid": "3c421312-c680-4ba7-a975-ad3aa6c97508", + "token": "k4u1Htxr5v--kdRu", + "name": "SG3", + "cpu_name": "AMD Ryzen 9 7950X 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 1, + "os": "Ubuntu 23.04", + "kernel_version": "6.2.0-39-generic", + "gpu_name": "None", + "ipv4": "77.93.90.94", + "ipv6": "2401:3bc0:600:121:be24:11ff:fe0c:da33", + "region": "🇸🇬", + "mem_total": 2054475776, + "swap_total": 0, + "disk_total": 21042757632, + "version": "1.1.93", + "weight": 26, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-02T00:00:00+08:00", + "group": "", + "tags": "", + "hidden": false, + "traffic_limit": 8796093022208, + "traffic_limit_type": "max", + "created_at": "2025-10-05T15:32:09+08:00", + "updated_at": "2026-04-21T11:05:31+08:00" + }, + { + "uuid": "97c332d6-57f9-43a9-90bf-3075885e9269", + "token": "BvT5Ru0czPydD0d_", + "name": "HK16", + "cpu_name": "AMD EPYC 7663 56-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 23.04", + "kernel_version": "6.2.0-39-generic", + "gpu_name": "None", + "ipv4": "185.241.42.105", + "ipv6": "2a14:4900:2202:aea::333", + "region": "🇭🇰", + "mem_total": 4097097728, + "swap_total": 0, + "disk_total": 42178375680, + "version": "1.1.93", + "weight": 28, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-03T00:00:00+08:00", + "group": "", + "tags": "", + "hidden": false, + "traffic_limit": 6597069766656, + "traffic_limit_type": "max", + "created_at": "2025-10-05T15:32:22+08:00", + "updated_at": "2026-04-21T11:05:18+08:00" + }, + { + "uuid": "85462e9c-b457-4f64-9822-854dc1c8aee1", + "token": "mOLftB7gHDRraZVm", + "name": "US7", + "cpu_name": "AMD Ryzen 9 9950X3D 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 23.04", + "kernel_version": "6.2.0-39-generic", + "gpu_name": "None", + "ipv4": "167.253.97.183", + "ipv6": "2602:f988:92:f2a::ba8", + "region": "🇺🇸", + "mem_total": 4097089536, + "swap_total": 0, + "disk_total": 42178375680, + "version": "1.1.93", + "weight": 27, + "price": -1, + "billing_cycle": 365, + "auto_renewal": true, + "currency": "$", + "expired_at": "2026-10-03T00:00:00+08:00", + "group": "", + "tags": "", + "hidden": false, + "traffic_limit": 13194139533312, + "traffic_limit_type": "max", + "created_at": "2025-10-05T15:32:36+08:00", + "updated_at": "2026-04-21T11:05:22+08:00" + }, + { + "uuid": "0981cb00-55f3-4619-b0ee-77fee21ffff5", + "token": "iexKPeh2q3B41QUgHmOzep", + "name": "US1", + "cpu_name": "AMD EPYC 7663 56-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.152.42", + "region": "🇺🇸", + "mem_total": 1945264128, + "swap_total": 0, + "disk_total": 10485227520, + "version": "1.1.38", + "weight": 0, + "price": 0, + "billing_cycle": 30, + "auto_renewal": true, + "currency": "$", + "expired_at": "2225-12-02T00:00:00+08:00", + "group": "", + "tags": "月", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2025-12-02T12:45:16+08:00", + "updated_at": "2026-04-21T11:04:43+08:00" + }, + { + "uuid": "aaaefea7-e89a-4abf-a1b4-006b6ace6c3d", + "token": "l0SXqNpQ9WUYj0Ic4eyszC", + "name": "US8", + "cpu_name": "AMD EPYC 7663 56-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.152.41", + "region": "🇺🇸", + "mem_total": 4056018944, + "swap_total": 0, + "disk_total": 21054046208, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "nube", + "tags": "月", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:49:59+08:00", + "updated_at": "2026-04-21T11:07:19+08:00" + }, + { + "uuid": "85208d82-36c1-4311-9ca1-ef691503fa76", + "token": "3rmITcyifBsG2eXV5NDbrp", + "name": "SG4", + "cpu_name": "AMD EPYC 7663 56-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.140.32", + "region": "🇸🇬", + "mem_total": 4055998464, + "swap_total": 0, + "disk_total": 21054046208, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "nube", + "tags": "月", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:07+08:00", + "updated_at": "2026-04-21T11:08:37+08:00" + }, + { + "uuid": "9699b9e2-74ab-443d-98a3-8ceb0a31c33d", + "token": "mEwmIhqDYprtOtXS72y6a1", + "name": "JP5", + "cpu_name": "AMD EPYC 7C13 64-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.148.218", + "region": "🇯🇵", + "mem_total": 4055994368, + "swap_total": 0, + "disk_total": 21054046208, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "nube", + "tags": "月", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:15+08:00", + "updated_at": "2026-04-21T11:06:57+08:00" + }, + { + "uuid": "0d947d8b-d61e-4372-9e2b-bf58a8a40f4c", + "token": "IsFL0as3CNreYWnLbq1NVO", + "name": "FR1", + "cpu_name": "AMD EPYC 7C13 64-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.157.103", + "ipv6": "2a01:4240::11:5011", + "region": "🇩🇪", + "mem_total": 4056010752, + "swap_total": 0, + "disk_total": 21054046208, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "nube", + "tags": "月", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:22+08:00", + "updated_at": "2026-04-21T11:08:26+08:00" + }, + { + "uuid": "3eb33f51-3e5d-4996-ab5c-0c91feb065bb", + "token": "1EPcGoTNsKaWL8x1y7ELqH", + "name": "JP6", + "cpu_name": "AMD EPYC 7443P 24-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 4, + "os": "Ubuntu 24.04 LTS", + "kernel_version": "6.8.0-35-generic", + "gpu_name": "None", + "ipv4": "154.31.113.136", + "ipv6": "2403:18c0:1000:5b:eccb:c7ff:fe85:1ee0", + "region": "🇯🇵", + "mem_total": 4105535488, + "swap_total": 1073737728, + "disk_total": 84446990336, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "dmit", + "tags": "月", + "hidden": false, + "traffic_limit": 17592186044416, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:31+08:00", + "updated_at": "2026-04-21T11:08:59+08:00" + }, + { + "uuid": "122c256a-adf4-468a-9bbc-7ed784ef3988", + "token": "bX4VaCnEjZoj6KG5nUOe8E", + "name": "HK18", + "cpu_name": "AMD EPYC 7C13 64-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 4, + "os": "Ubuntu 24.04.3 LTS", + "kernel_version": "6.14.0-37-generic", + "gpu_name": "None", + "ipv4": "154.3.39.160", + "ipv6": "2403:18c0:5:175:be24:11ff:fe9f:38d", + "region": "🇭🇰", + "mem_total": 4105945088, + "swap_total": 1073737728, + "disk_total": 84446990336, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "dmit", + "tags": "月", + "hidden": false, + "traffic_limit": 17592186044416, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:40+08:00", + "updated_at": "2026-04-21T11:09:04+08:00" + }, + { + "uuid": "5e00526a-a875-4111-8fad-a7401b879b39", + "token": "bFMT3HbTfy52OJQVCkAwsf", + "name": "US9", + "cpu_name": "AMD EPYC 9655 96-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04 LTS", + "kernel_version": "6.8.0-35-generic", + "gpu_name": "None", + "ipv4": "216.234.142.116", + "ipv6": "2605:52c0:3:2e4:be24:11ff:fe0e:75ba", + "region": "🇺🇸", + "mem_total": 4105211904, + "swap_total": 1073737728, + "disk_total": 84446990336, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "dmit", + "tags": "月", + "hidden": false, + "traffic_limit": 10995116277760, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:48+08:00", + "updated_at": "2026-04-21T11:09:00+08:00" + }, + { + "uuid": "f1f0d805-89f4-4a28-b805-5a237bf0416e", + "token": "BqyvafvcG39zHPK82FO5WB", + "name": "JP7", + "cpu_name": "AMD Ryzen 9 7950X 16-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 23.04", + "kernel_version": "6.2.0-39-generic", + "gpu_name": "None", + "ipv4": "162.141.130.46", + "ipv6": "2602:f988:9a:d9::d", + "region": "🇯🇵", + "mem_total": 4097089536, + "swap_total": 0, + "disk_total": 31611748352, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "dmit", + "tags": "月", + "hidden": false, + "traffic_limit": 13194139533312, + "traffic_limit_type": "max", + "created_at": "2026-01-19T08:50:57+08:00", + "updated_at": "2026-04-21T11:05:15+08:00" + }, + { + "uuid": "a3294937-a389-434e-9736-63f9a9564171", + "token": "pecVAvXFxdPlSP5Z1vcKUo", + "name": "HK17", + "cpu_name": "AMD EPYC 7713 64-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-101-generic", + "gpu_name": "None", + "ipv4": "64.118.132.129", + "region": "🇭🇰", + "mem_total": 4056018944, + "swap_total": 0, + "disk_total": 21054046208, + "version": "1.1.40", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "", + "tags": "月", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-01-20T12:07:18+08:00", + "updated_at": "2026-04-21T11:09:02+08:00" + }, + { + "uuid": "3e82d551-702b-4d5a-8daa-d18f168ac6db", + "token": "jkYkK9OSm9tU35LqAqX0DV", + "name": "Runner|CI", + "cpu_name": "Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 2, + "os": "Ubuntu 24.04 LTS", + "kernel_version": "6.8.0-106-generic", + "gpu_name": "None", + "ipv4": "172.245.205.188", + "region": "🇺🇸", + "mem_total": 4106117120, + "swap_total": 2147479552, + "disk_total": 37952634880, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "开发配件", + "tags": "", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:21:20+08:00", + "updated_at": "2026-04-21T11:07:49+08:00" + }, + { + "uuid": "5484e5a2-bfe7-45fc-9f4c-855bf688bf9c", + "token": "13gyO5Ovyik66Q6IrJ2cDc", + "name": "Git|Plan", + "cpu_name": "Intel(R) Xeon(R) CPU E5-2699 v4 @ 2.20GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 4, + "os": "AlmaLinux 8.9 (Midnight Oncilla)", + "kernel_version": "4.18.0-513.5.1.el8_9.x86_64", + "gpu_name": "None", + "ipv4": "204.44.110.218", + "region": "🇺🇸", + "mem_total": 8326758400, + "swap_total": 8589930496, + "disk_total": 97114132480, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "开发配件", + "tags": "", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:23:36+08:00", + "updated_at": "2026-04-21T11:04:54+08:00" + }, + { + "uuid": "0b287ac3-9721-4485-a3a0-3ccfb7d83b58", + "token": "RVs9QtvL4ovGAL69ULibdQ", + "name": "探针|Plan|文件", + "cpu_name": "Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 4, + "os": "Ubuntu 22.04.1 LTS", + "kernel_version": "5.15.0-46-generic", + "gpu_name": "None", + "ipv4": "107.173.50.22", + "region": "🇺🇸", + "mem_total": 8335654912, + "swap_total": 8589930496, + "disk_total": 97140543488, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "开发配件", + "tags": "", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:24:51+08:00", + "updated_at": "2026-04-21T11:05:22+08:00" + }, + { + "uuid": "33ee150d-66bf-4bc8-8ab3-85d15b017746", + "token": "Xf7OAsnsT55gE6Ks2ybCyD", + "name": "邮箱系统", + "cpu_name": "Intel(R) Xeon(R) Gold 6152 CPU @ 2.10GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 4, + "os": "Ubuntu 24.04 LTS", + "kernel_version": "6.8.0-107-generic", + "gpu_name": "None", + "ipv4": "192.210.150.114", + "region": "🇺🇸", + "mem_total": 4105392128, + "swap_total": 2147479552, + "disk_total": 82340294656, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "运营系统", + "tags": "", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:25:38+08:00", + "updated_at": "2026-04-21T11:06:32+08:00" + }, + { + "uuid": "ce05ca5f-ddab-4828-90fd-b9f8a4e57ebb", + "token": "Ht7EZriFxlF4j4acWfrd1P", + "name": "正式VPN面板", + "cpu_name": "AMD EPYC 7K62 48-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 12, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-110-generic", + "gpu_name": "None", + "ipv4": "103.150.215.44", + "region": "🇺🇸", + "mem_total": 16767209472, + "swap_total": 0, + "disk_total": 124787648512, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "正式环境", + "tags": "vpn", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:26:45+08:00", + "updated_at": "2026-04-21T11:06:10+08:00" + }, + { + "uuid": "8003223b-b8f4-433f-9f2b-b5b67300ca5f", + "token": "7ohm9YLRvcm7UxlyUbKBht", + "name": "正式机场面板", + "cpu_name": "AMD EPYC 7K62 48-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 12, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-110-generic", + "gpu_name": "None", + "ipv4": "103.150.215.40", + "region": "🇺🇸", + "mem_total": 16767209472, + "swap_total": 0, + "disk_total": 124787648512, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "正式环境", + "tags": "机场", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:28:13+08:00", + "updated_at": "2026-04-21T11:07:43+08:00" + }, + { + "uuid": "1dca2df7-01ae-4e58-88ae-b19ecac75923", + "token": "Izdi0WA5Wed2fLBIzwuXy1", + "name": "正式VPN面板|备用", + "cpu_name": "Intel(R) Xeon(R) Gold 6152 CPU @ 2.10GHz", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 6, + "os": "Ubuntu 24.04 LTS", + "kernel_version": "6.8.0-31-generic", + "gpu_name": "None", + "ipv4": "104.168.98.173", + "region": "🇺🇸", + "mem_total": 8326410240, + "swap_total": 4294963200, + "disk_total": 154205229056, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "正式环境", + "tags": "vpn", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:30:12+08:00", + "updated_at": "2026-04-21T11:05:47+08:00" + }, + { + "uuid": "e39789b2-0bb7-4a21-a92a-3020718a801e", + "token": "VELMC0WL3wWw39xWYP03YT", + "name": "VPN面板|测试", + "cpu_name": "AMD EPYC 7K62 48-Core Processor", + "virtualization": "kvm", + "arch": "amd64", + "cpu_cores": 4, + "os": "Ubuntu 24.04.1 LTS", + "kernel_version": "6.8.0-110-generic", + "gpu_name": "None", + "ipv4": "154.12.35.103", + "region": "🇺🇸", + "mem_total": 8326950912, + "swap_total": 0, + "disk_total": 72757446656, + "version": "1.1.93", + "weight": 0, + "price": 0, + "billing_cycle": 0, + "auto_renewal": false, + "currency": "$", + "expired_at": null, + "group": "测试环境", + "tags": "vpn", + "hidden": false, + "traffic_limit": 0, + "traffic_limit_type": "max", + "created_at": "2026-02-10T13:31:14+08:00", + "updated_at": "2026-04-21T11:07:36+08:00" + } +]