feat(apple支付): 添加按平台查询支付方式和恢复交易逻辑优化
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m52s

添加FindListByPlatform方法用于按平台查询支付方式
优化apple支付恢复交易逻辑,支持直接使用交易ID查询
添加API配置处理逻辑和错误回退机制
This commit is contained in:
shanshanzhong 2025-12-16 18:58:55 -08:00
parent 5bc453b09f
commit 40a45199a5
2 changed files with 83 additions and 6 deletions

View File

@ -2,9 +2,13 @@ package apple
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/google/uuid"
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/internal/model/payment"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
@ -12,7 +16,6 @@ import (
iapapple "github.com/perfect-panel/server/pkg/iap/apple"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/google/uuid"
"github.com/pkg/errors"
"gorm.io/gorm"
)
@ -37,19 +40,85 @@ func (l *RestoreLogic) Restore(req *types.RestoreAppleTransactionsRequest) error
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access")
}
pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData)
// Try to load payment config to get API credentials
var apiCfg iapapple.ServerAPIConfig
// We need to find *any* apple payment config to get credentials.
// In most cases, there is only one apple payment method.
// We can try to find by platform "apple"
payMethods, err := l.svcCtx.PaymentModel.FindListByPlatform(l.ctx, "apple")
if err == nil && len(payMethods) > 0 {
// Use the first available config
pay := payMethods[0]
var cfg payment.AppleIAPConfig
if err := cfg.Unmarshal([]byte(pay.Config)); err == nil {
apiCfg = iapapple.ServerAPIConfig{
KeyID: cfg.KeyID,
IssuerID: cfg.IssuerID,
PrivateKey: cfg.PrivateKey,
Sandbox: cfg.Sandbox,
}
// Fix private key format if needed (same as in attachTransactionByIdLogic)
if !strings.Contains(apiCfg.PrivateKey, "\n") && strings.Contains(apiCfg.PrivateKey, "BEGIN PRIVATE KEY") {
apiCfg.PrivateKey = strings.ReplaceAll(apiCfg.PrivateKey, " ", "\n")
apiCfg.PrivateKey = strings.ReplaceAll(apiCfg.PrivateKey, "-----BEGIN\nPRIVATE\nKEY-----", "-----BEGIN PRIVATE KEY-----")
apiCfg.PrivateKey = strings.ReplaceAll(apiCfg.PrivateKey, "-----END\nPRIVATE\nKEY-----", "-----END PRIVATE KEY-----")
}
}
}
// Fallback credentials if missing (dev/debug)
if apiCfg.PrivateKey == "" {
apiCfg.PrivateKey = `-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgsVDj0g/D7uNCm8aC
E4TuaiDT4Pgb1IuuZ69YdGNvcAegCgYIKoZIzj0DAQehRANCAARObgGumaESbPMM
SIRDAVLcWemp0fMlnfDE4EHmqcD58arEJWsr3aWEhc4BHocOUIGjko0cVWGchrFa
/T/KG1tr
-----END PRIVATE KEY-----`
apiCfg.KeyID = "2C4X3HVPM8"
}
if apiCfg.IssuerID == "" {
apiCfg.IssuerID = "34f54810-5118-4b7f-8069-c8c1e012b7a9"
}
// Try to get BundleID
if apiCfg.BundleID == "" && l.svcCtx.Config.Site.CustomData != "" {
var customData struct {
IapBundleId string `json:"iapBundleId"`
}
_ = json.Unmarshal([]byte(l.svcCtx.Config.Site.CustomData), &customData)
apiCfg.BundleID = customData.IapBundleId
}
return l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
for _, j := range req.Transactions {
txp, err := iapapple.ParseTransactionJWS(j)
if err != nil {
for _, txID := range req.Transactions {
// 1. Try to verify as JWS first (if client sends JWS)
var txp *iapapple.TransactionPayload
var err error
// Try to parse as JWS
if len(txID) > 50 && (strings.Contains(txID, ".") || strings.HasPrefix(txID, "ey")) {
txp, err = iapapple.VerifyTransactionJWS(txID)
} else {
// 2. If not JWS, treat as TransactionID and fetch from Apple
var jws string
jws, err = iapapple.GetTransactionInfo(apiCfg, txID)
if err == nil {
txp, err = iapapple.VerifyTransactionJWS(jws)
}
}
if err != nil || txp == nil {
l.Errorw("restore: invalid transaction", logger.Field("id", txID), logger.Field("error", err))
continue
}
m, ok := pm.Items[txp.ProductId]
if !ok {
continue
}
// Check if already processed
_, e := iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txp.OriginalTransactionId)
if e == nil {
continue
continue // Already processed, skip
}
iapTx := &iapmodel.Transaction{
UserId: u.Id,
@ -83,4 +152,3 @@ func (l *RestoreLogic) Restore(req *types.RestoreAppleTransactionsRequest) error
return nil
})
}

View File

@ -12,6 +12,7 @@ type customPaymentLogicModel interface {
FindAll(ctx context.Context) ([]*Payment, error)
FindListByPage(ctx context.Context, page, size int, req *Filter) (int64, []*Payment, error)
FindAvailableMethods(ctx context.Context) ([]*Payment, error)
FindListByPlatform(ctx context.Context, platform string) ([]*Payment, error)
}
// NewModel returns a model for the database table.
@ -21,6 +22,14 @@ func NewModel(conn *gorm.DB, c *redis.Client) Model {
}
}
func (m *customPaymentModel) FindListByPlatform(ctx context.Context, platform string) ([]*Payment, error) {
var resp []*Payment
err := m.QueryNoCacheCtx(ctx, &resp, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Payment{}).Where("platform = ?", platform).Find(v).Error
})
return resp, err
}
func (m *customPaymentModel) FindOneByPaymentToken(ctx context.Context, token string) (*Payment, error) {
var resp *Payment
key := cachePaymentTokenPrefix + token