无订阅 支付后出现两个订阅
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m15s

This commit is contained in:
shanshanzhong 2026-03-05 07:59:49 -08:00
parent ce6351368d
commit a1ab0fefa4
3 changed files with 270 additions and 1030 deletions

View File

@ -34,6 +34,12 @@ type AttachTransactionLogic struct {
svcCtx *svc.ServiceContext
}
const (
orderTypeSubscribe uint8 = 1
orderStatusPending uint8 = 1
orderStatusPaid uint8 = 2
)
func NewAttachTransactionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttachTransactionLogic {
return &AttachTransactionLogic{
Logger: logger.WithContext(ctx),
@ -52,6 +58,30 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
if err := commonLogic.DenyIfFamilyMemberReadonly(l.ctx, l.svcCtx.DB, u.Id); err != nil {
return nil, err
}
if strings.TrimSpace(req.OrderNo) == "" {
l.Errorw("参数错误orderNo 不能为空")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "order_no is required")
}
orderInfo, orderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
switch {
case errors.Is(orderErr, gorm.ErrRecordNotFound):
l.Errorw("订单不存在", logger.Field("orderNo", req.OrderNo))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist")
case orderErr != nil:
l.Errorw("查询订单失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", orderErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", orderErr.Error())
case orderInfo == nil || orderInfo.Id == 0:
l.Errorw("订单不存在", logger.Field("orderNo", req.OrderNo))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist")
case orderInfo.UserId != u.Id:
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")
}
isNewPurchaseOrder := orderInfo.Type == orderTypeSubscribe
if isNewPurchaseOrder {
l.Infow("首购订单将只由订单激活流程创建订阅", logger.Field("orderNo", req.OrderNo), logger.Field("orderType", orderInfo.Type))
}
txPayload, err := iapapple.VerifyTransactionJWS(req.SignedTransactionJWS)
if err != nil {
l.Errorw("JWS 验签失败", logger.Field("error", err.Error()))
@ -62,21 +92,6 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
var existTx *iapmodel.Transaction
existTx, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
l.Infow("幂等等检查", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("exists", existTx != nil && existTx.Id > 0))
var orderInfo *ordermodel.Order
if req.OrderNo != "" {
ord, orderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
switch {
case orderErr == nil && ord != nil && ord.Id > 0:
if ord.UserId != 0 && ord.UserId != u.Id {
l.Errorw("订单与当前用户不匹配", logger.Field("orderNo", req.OrderNo), logger.Field("orderUserId", ord.UserId), logger.Field("userId", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order owner mismatch")
}
orderInfo = ord
case orderErr != nil && !errors.Is(orderErr, gorm.ErrRecordNotFound):
l.Errorw("查询订单失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", orderErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order error: %v", orderErr.Error())
}
}
// 解析 Apple 商品ID中的单位与数量支持 dayN / monthN / yearN
var parsedUnit string
@ -168,13 +183,9 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}
if subscribeId == 0 {
// fallback from order_no if provided
if orderInfo != nil && orderInfo.Id > 0 {
duration = orderInfo.Quantity
subscribeId = orderInfo.SubscribeId
l.Infow("使用订单信息回退", logger.Field("orderNo", req.OrderNo), logger.Field("durationDays", duration), logger.Field("subscribeId", subscribeId))
} else if req.OrderNo != "" {
l.Infow("订单信息不可用,尝试请求参数回退", logger.Field("orderNo", req.OrderNo))
}
duration = orderInfo.Quantity
subscribeId = orderInfo.SubscribeId
l.Infow("使用订单信息回退", logger.Field("orderNo", req.OrderNo), logger.Field("durationDays", duration), logger.Field("subscribeId", subscribeId))
// final fallback: use request fields
if duration <= 0 {
duration = req.DurationDays
@ -194,7 +205,7 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
exp := iapapple.CalcExpire(txPayload.PurchaseDate, duration)
l.Infow("计算订阅到期时间", logger.Field("expireAt", exp), logger.Field("expireUnix", exp.Unix()))
var orderLinkedSub *user.Subscribe
if orderInfo != nil && orderInfo.SubscribeToken != "" {
if !isNewPurchaseOrder && orderInfo != nil && orderInfo.SubscribeToken != "" {
orderSub, subErr := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, orderInfo.SubscribeToken)
switch {
case subErr == nil && orderSub != nil && orderSub.Id > 0:
@ -211,7 +222,7 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}
}
var singleModeAnchorSub *user.Subscribe
if l.svcCtx.Config.Subscribe.SingleModel && orderLinkedSub == nil {
if !isNewPurchaseOrder && l.svcCtx.Config.Subscribe.SingleModel && orderLinkedSub == nil {
anchorSub, anchorErr := findSingleModeMergeTarget(l.ctx, l.svcCtx, u.Id, subscribeId)
switch {
case errors.Is(anchorErr, commonLogic.ErrSingleModePlanMismatch):
@ -229,6 +240,15 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}
if existTx != nil && existTx.Id > 0 {
if isNewPurchaseOrder {
if syncErr := l.syncOrderStatusAndEnqueue(orderInfo); syncErr != nil {
l.Errorw("事务已处理但同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", syncErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "sync order status failed: %v", syncErr.Error())
}
l.Infow("事务已处理,首购订单等待激活队列发放订阅", logger.Field("orderNo", req.OrderNo), logger.Field("expiresAt", exp.Unix()))
return &types.AttachAppleTransactionResponse{ExpiresAt: exp.Unix(), Tier: tier}, nil
}
existSub, err := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId)
switch {
case err == nil && existSub != nil && existSub.Id > 0:
@ -286,68 +306,46 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}
l.Infow("写入事务表成功", logger.Field("id", iapTx.Id))
}
merged := false
if orderLinkedSub != nil {
if _, e := l.extendSubscribeForIAP(orderLinkedSub, exp, subscribeId, tx); e != nil {
l.Errorw("更新订单关联订阅失败", logger.Field("error", e.Error()), logger.Field("userSubscribeId", orderLinkedSub.Id))
return e
}
merged = true
} else if singleModeAnchorSub != nil {
if _, e := l.extendSubscribeForIAP(singleModeAnchorSub, exp, subscribeId, tx); e != nil {
l.Errorw("更新单订阅锚点失败", logger.Field("error", e.Error()), logger.Field("userSubscribeId", singleModeAnchorSub.Id))
return e
}
merged = true
}
if !merged {
userSub := user.Subscribe{
UserId: u.Id,
SubscribeId: subscribeId,
StartTime: time.Now(),
ExpireTime: exp,
Traffic: 0,
Download: 0,
Upload: 0,
Token: fmt.Sprintf("iap:%s", txPayload.OriginalTransactionId),
UUID: uuid.New().String(),
Status: 1,
}
if e := l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx); e != nil {
l.Errorw("写入用户订阅失败", logger.Field("error", e.Error()))
return e
}
l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix()))
}
// optional: mark related order as paid and enqueue activation
if req.OrderNo != "" {
if orderInfo == nil {
if ord, e := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo); e == nil && ord != nil && ord.Id > 0 {
orderInfo = ord
}
}
if orderInfo == nil {
// do not fail transaction if order not found; just continue
l.Infow("订单不存在或查询失败,跳过订单状态更新", logger.Field("orderNo", req.OrderNo))
return nil
}
if orderInfo.Status == 1 {
if e := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OrderNo, 2, tx); e != nil {
l.Errorw("更新订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error()))
if !isNewPurchaseOrder {
merged := false
if orderLinkedSub != nil {
if _, e := l.extendSubscribeForIAP(orderLinkedSub, exp, subscribeId, tx); e != nil {
l.Errorw("更新订单关联订阅失败", logger.Field("error", e.Error()), logger.Field("userSubscribeId", orderLinkedSub.Id))
return e
}
l.Infow("更新订单状态成功", logger.Field("orderNo", req.OrderNo), logger.Field("status", 2))
merged = true
} else if singleModeAnchorSub != nil {
if _, e := l.extendSubscribeForIAP(singleModeAnchorSub, exp, subscribeId, tx); e != nil {
l.Errorw("更新单订阅锚点失败", logger.Field("error", e.Error()), logger.Field("userSubscribeId", singleModeAnchorSub.Id))
return e
}
merged = true
}
// enqueue activation regardless (idempotent handler downstream)
payload := queueType.ForthwithActivateOrderPayload{OrderNo: req.OrderNo}
bytes, _ := json.Marshal(payload)
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
if _, e := l.svcCtx.Queue.EnqueueContext(l.ctx, task); e != nil {
// non-fatal
l.Errorw("enqueue activate task error", logger.Field("error", e.Error()))
} else {
l.Infow("已加入订单激活队列", logger.Field("orderNo", req.OrderNo))
if !merged {
userSub := user.Subscribe{
UserId: u.Id,
SubscribeId: subscribeId,
StartTime: time.Now(),
ExpireTime: exp,
Traffic: 0,
Download: 0,
Upload: 0,
Token: fmt.Sprintf("iap:%s", txPayload.OriginalTransactionId),
UUID: uuid.New().String(),
Status: 1,
}
if e := l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx); e != nil {
l.Errorw("写入用户订阅失败", logger.Field("error", e.Error()))
return e
}
l.Infow("写入用户订阅成功", logger.Field("userId", u.Id), logger.Field("subscribeId", subscribeId), logger.Field("expireUnix", exp.Unix()))
}
} else {
l.Infow("首购订单跳过 attach 阶段订阅写入", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderType", orderInfo.Type))
}
if e := l.syncOrderStatusAndEnqueue(orderInfo, tx); e != nil {
l.Errorw("同步订单状态失败", logger.Field("orderNo", req.OrderNo), logger.Field("error", e.Error()))
return e
}
return nil
})
@ -362,6 +360,30 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}, nil
}
func (l *AttachTransactionLogic) syncOrderStatusAndEnqueue(orderInfo *ordermodel.Order, tx ...*gorm.DB) error {
if orderInfo == nil || orderInfo.OrderNo == "" {
return errors.New("order info is nil")
}
if orderInfo.Status == orderStatusPending {
if err := l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, orderInfo.OrderNo, orderStatusPaid, tx...); err != nil {
return err
}
orderInfo.Status = orderStatusPaid
l.Infow("更新订单状态成功", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("status", orderStatusPaid))
}
// enqueue activation regardless (idempotent handler downstream)
payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo}
bytes, _ := json.Marshal(payload)
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task); err != nil {
// non-fatal
l.Errorw("enqueue activate task error", logger.Field("error", err.Error()))
} else {
l.Infow("已加入订单激活队列", logger.Field("orderNo", orderInfo.OrderNo))
}
return nil
}
func (l *AttachTransactionLogic) findIAPSubscribeByOriginalTransactionID(originalTransactionID string) (*user.Subscribe, error) {
if originalTransactionID == "" {
return nil, gorm.ErrRecordNotFound

View File

@ -3165,7 +3165,7 @@ type AttachAppleTransactionRequest struct {
DurationDays int64 `json:"duration_days,omitempty"`
Tier string `json:"tier,omitempty"`
SubscribeId int64 `json:"subscribe_id,omitempty"`
OrderNo string `json:"order_no,omitempty"`
OrderNo string `json:"order_no" validate:"required"`
}
type AttachAppleTransactionResponse struct {

File diff suppressed because one or more lines are too long