家庭组逻辑导致支付失败
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m11s

This commit is contained in:
shanshanzhong 2026-03-05 03:20:38 -08:00
parent 9f4d71770b
commit ce6351368d
5 changed files with 1193 additions and 44 deletions

View File

@ -17,7 +17,7 @@ Logger: # 日志配置
MySQL:
Addr: 127.0.0.1:3306 # MySQL地址
Username: root # MySQL用户名 (与创建的用户一致)
Password: password # MySQL密码 (换成之前生成的随机密码)
Password: rootpassword # MySQL密码 (换成之前生成的随机密码)
Dbname: ppanel # MySQL数据库名 (与脚本创建的数据库一致)
Config: charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
MaxIdleConns: 10

View File

@ -14,6 +14,7 @@ import (
"github.com/hibiken/asynq"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
ordermodel "github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
@ -61,6 +62,21 @@ 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
@ -152,14 +168,12 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}
if subscribeId == 0 {
// fallback from order_no if provided
if req.OrderNo != "" {
if ord, e := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo); e == nil && ord != nil && ord.Id != 0 {
duration = ord.Quantity
subscribeId = ord.SubscribeId
l.Infow("使用订单信息回退", logger.Field("orderNo", req.OrderNo), logger.Field("durationDays", duration), logger.Field("subscribeId", subscribeId))
} else {
l.Infow("订单信息不可用,尝试请求参数回退", logger.Field("orderNo", req.OrderNo))
}
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))
}
// final fallback: use request fields
if duration <= 0 {
@ -179,25 +193,49 @@ 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 != "" {
orderSub, subErr := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, orderInfo.SubscribeToken)
switch {
case subErr == nil && orderSub != nil && orderSub.Id > 0:
if orderSub.UserId != u.Id {
l.Errorw("订单订阅与当前用户不匹配", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("orderSubUserId", orderSub.UserId), logger.Field("userId", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "order subscribe owner mismatch")
}
orderLinkedSub = orderSub
subscribeId = orderSub.SubscribeId
l.Infow("IAP 绑定命中订单订阅", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("userSubscribeId", orderSub.Id), logger.Field("subscribeToken", orderInfo.SubscribeToken))
case subErr != nil && !errors.Is(subErr, gorm.ErrRecordNotFound):
l.Errorw("查询订单订阅失败", logger.Field("orderNo", orderInfo.OrderNo), logger.Field("subscribeToken", orderInfo.SubscribeToken), logger.Field("error", subErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find order subscribe error: %v", subErr.Error())
}
}
var singleModeAnchorSub *user.Subscribe
if l.svcCtx.Config.Subscribe.SingleModel && orderLinkedSub == nil {
anchorSub, anchorErr := findSingleModeMergeTarget(l.ctx, l.svcCtx, u.Id, subscribeId)
switch {
case errors.Is(anchorErr, commonLogic.ErrSingleModePlanMismatch):
l.Errorw("单订阅模式下 IAP 套餐不匹配", logger.Field("userId", u.Id), logger.Field("orderNo", req.OrderNo), logger.Field("iapSubscribeId", subscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribePlanMismatch), "single subscribe mode plan mismatch")
case anchorErr == nil && anchorSub != nil && anchorSub.Id > 0:
singleModeAnchorSub = anchorSub
subscribeId = anchorSub.SubscribeId
l.Infow("IAP 绑定命中单订阅锚点", logger.Field("userSubscribeId", anchorSub.Id), logger.Field("subscribeId", anchorSub.SubscribeId))
case errors.Is(anchorErr, gorm.ErrRecordNotFound):
case anchorErr != nil:
l.Errorw("查询单订阅锚点失败", logger.Field("userId", u.Id), logger.Field("error", anchorErr.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find single mode anchor subscribe error: %v", anchorErr.Error())
}
}
if existTx != nil && existTx.Id > 0 {
token := fmt.Sprintf("iap:%s", txPayload.OriginalTransactionId)
existSub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token)
existSub, err := l.findIAPSubscribeByOriginalTransactionID(txPayload.OriginalTransactionId)
switch {
case err == nil && existSub != nil && existSub.Id > 0:
newExpire := existSub.ExpireTime
if exp.After(newExpire) {
existSub.ExpireTime = exp
newExpire = exp
}
if subscribeId > 0 && existSub.SubscribeId != subscribeId {
existSub.SubscribeId = subscribeId
}
existSub.Status = 1
existSub.FinishedAt = nil
if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, existSub); err != nil {
l.Errorw("刷新 IAP 订阅失败", logger.Field("error", err.Error()), logger.Field("subscribeId", existSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update iap subscribe failed: %v", err.Error())
newExpire, updateErr := l.extendSubscribeForIAP(existSub, exp, subscribeId)
if updateErr != nil {
l.Errorw("刷新 IAP 订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", existSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update iap subscribe failed: %v", updateErr.Error())
}
l.Infow("事务已处理,刷新订阅到期时间", logger.Field("originalTransactionId", txPayload.OriginalTransactionId), logger.Field("tier", tier), logger.Field("expiresAt", newExpire.Unix()))
return &types.AttachAppleTransactionResponse{
@ -205,9 +243,27 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
Tier: tier,
}, nil
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
l.Errorw("查询 IAP 订阅失败", logger.Field("error", err.Error()), logger.Field("token", token))
l.Errorw("查询 IAP 订阅失败", logger.Field("error", err.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find iap subscribe error: %v", err.Error())
}
if orderLinkedSub != nil {
newExpire, updateErr := l.extendSubscribeForIAP(orderLinkedSub, exp, subscribeId)
if updateErr != nil {
l.Errorw("刷新订单关联订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", orderLinkedSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update order subscribe failed: %v", updateErr.Error())
}
l.Infow("事务已处理,刷新订单关联订阅到期时间", logger.Field("orderNo", req.OrderNo), logger.Field("userSubscribeId", orderLinkedSub.Id), logger.Field("expiresAt", newExpire.Unix()))
return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil
}
if singleModeAnchorSub != nil {
newExpire, updateErr := l.extendSubscribeForIAP(singleModeAnchorSub, exp, subscribeId)
if updateErr != nil {
l.Errorw("刷新单订阅锚点订阅失败", logger.Field("error", updateErr.Error()), logger.Field("subscribeId", singleModeAnchorSub.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update single mode anchor subscribe failed: %v", updateErr.Error())
}
l.Infow("事务已处理,刷新单订阅锚点到期时间", logger.Field("userSubscribeId", singleModeAnchorSub.Id), logger.Field("expiresAt", newExpire.Unix()))
return &types.AttachAppleTransactionResponse{ExpiresAt: newExpire.Unix(), Tier: tier}, nil
}
}
sum := sha256.Sum256([]byte(req.SignedTransactionJWS))
@ -230,28 +286,47 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
}
l.Infow("写入事务表成功", logger.Field("id", iapTx.Id))
}
// insert user_subscribe
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,
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 e := l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx); e != nil {
l.Errorw("写入用户订阅失败", logger.Field("error", e.Error()))
return e
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()))
}
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 != "" {
orderInfo, e := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if e != nil {
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
@ -286,3 +361,40 @@ func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest
Tier: tier,
}, nil
}
func (l *AttachTransactionLogic) findIAPSubscribeByOriginalTransactionID(originalTransactionID string) (*user.Subscribe, error) {
if originalTransactionID == "" {
return nil, gorm.ErrRecordNotFound
}
candidates := []string{fmt.Sprintf("iap:%s", originalTransactionID), originalTransactionID}
for _, token := range candidates {
sub, err := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token)
if err == nil && sub != nil && sub.Id > 0 {
return sub, nil
}
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
}
return nil, gorm.ErrRecordNotFound
}
func (l *AttachTransactionLogic) extendSubscribeForIAP(userSub *user.Subscribe, exp time.Time, subscribeId int64, tx ...*gorm.DB) (time.Time, error) {
if userSub == nil {
return time.Time{}, errors.New("user subscribe is nil")
}
newExpire := userSub.ExpireTime
if exp.After(newExpire) {
newExpire = exp
}
userSub.ExpireTime = newExpire
if subscribeId > 0 && userSub.SubscribeId != subscribeId {
userSub.SubscribeId = subscribeId
}
userSub.Status = 1
userSub.FinishedAt = nil
if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, userSub, tx...); err != nil {
return time.Time{}, err
}
return newExpire, nil
}

View File

@ -3,6 +3,7 @@ package apple
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
@ -153,6 +154,26 @@ SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
// But for now, we only rely on appAccountToken or just skip order linking.
exp := iapapple.CalcExpire(txp.PurchaseDate, m.DurationDays)
if l.svcCtx.Config.Subscribe.SingleModel {
anchorSub, anchorErr := findSingleModeMergeTarget(l.ctx, l.svcCtx, u.Id, m.SubscribeId)
switch {
case errors.Is(anchorErr, commonLogic.ErrSingleModePlanMismatch):
return errors.Wrapf(xerr.NewErrCode(xerr.SingleSubscribePlanMismatch), "single subscribe mode plan mismatch")
case anchorErr == nil && anchorSub != nil && anchorSub.Id > 0:
if exp.After(anchorSub.ExpireTime) {
anchorSub.ExpireTime = exp
}
anchorSub.Status = 1
anchorSub.FinishedAt = nil
if err := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, anchorSub, tx); err != nil {
return err
}
continue
case errors.Is(anchorErr, gorm.ErrRecordNotFound):
case anchorErr != nil:
return anchorErr
}
}
userSub := user.Subscribe{
UserId: u.Id,
SubscribeId: m.SubscribeId,
@ -161,7 +182,7 @@ SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
Traffic: 0,
Download: 0,
Upload: 0,
Token: txp.OriginalTransactionId,
Token: fmt.Sprintf("iap:%s", txp.OriginalTransactionId),
UUID: uuid.New().String(),
Status: 1,
}

View File

@ -0,0 +1,65 @@
package apple
import (
"context"
"errors"
commonLogic "github.com/perfect-panel/server/internal/logic/common"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"gorm.io/gorm"
)
func findSingleModeMergeTarget(ctx context.Context, svcCtx *svc.ServiceContext, userId int64, subscribeId int64) (*user.Subscribe, error) {
anchorSub, err := svcCtx.UserModel.FindSingleModeAnchorSubscribe(ctx, userId)
switch {
case err == nil && anchorSub != nil && anchorSub.Id > 0:
if subscribeId > 0 && anchorSub.SubscribeId != subscribeId {
return nil, commonLogic.ErrSingleModePlanMismatch
}
return anchorSub, nil
case err != nil && !errors.Is(err, gorm.ErrRecordNotFound):
return nil, err
}
userSubs, queryErr := svcCtx.UserModel.QueryUserSubscribe(ctx, userId, 0, 1, 2, 3, 5)
if queryErr != nil {
return nil, queryErr
}
var candidate *user.SubscribeDetails
for _, item := range userSubs {
if item == nil {
continue
}
if subscribeId > 0 && item.SubscribeId != subscribeId {
continue
}
if candidate == nil ||
item.ExpireTime.After(candidate.ExpireTime) ||
(item.ExpireTime.Equal(candidate.ExpireTime) && item.UpdatedAt.After(candidate.UpdatedAt)) ||
(item.ExpireTime.Equal(candidate.ExpireTime) && item.UpdatedAt.Equal(candidate.UpdatedAt) && item.Id > candidate.Id) {
candidate = item
}
}
if candidate == nil {
return nil, gorm.ErrRecordNotFound
}
return &user.Subscribe{
Id: candidate.Id,
UserId: candidate.UserId,
OrderId: candidate.OrderId,
SubscribeId: candidate.SubscribeId,
StartTime: candidate.StartTime,
ExpireTime: candidate.ExpireTime,
FinishedAt: candidate.FinishedAt,
Traffic: candidate.Traffic,
Download: candidate.Download,
Upload: candidate.Upload,
Token: candidate.Token,
UUID: candidate.UUID,
Status: candidate.Status,
Note: candidate.Note,
CreatedAt: candidate.CreatedAt,
UpdatedAt: candidate.UpdatedAt,
}, nil
}

951
订单日志.txt Normal file

File diff suppressed because one or more lines are too long