x
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 4m44s

This commit is contained in:
shanshanzhong 2026-04-26 21:12:22 -07:00
parent 5b49aa8242
commit ab38cd4943
19 changed files with 2233 additions and 102 deletions

BIN
debug_device_login Executable file

Binary file not shown.

View File

@ -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:]
}

View File

@ -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)))

View File

@ -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
})

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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,

View File

@ -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,

View File

@ -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))

View File

@ -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,

View File

@ -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),
)...,
)
}

View File

@ -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
}

View File

@ -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"}

View File

@ -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...)
}
}

View File

@ -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,
},

View File

@ -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
}

View File

@ -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 <security_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)
}
}
}
}
}

File diff suppressed because it is too large Load Diff