hi-server/internal/logic/public/iap/apple/attachTransactionLogic.go
shanshanzhong 62186ca672
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m37s
feat(iap/apple): 实现苹果IAP非续期订阅功能
新增苹果IAP相关接口与逻辑,包括产品列表查询、交易绑定、状态查询和恢复购买功能。移除旧的IAP验证逻辑,重构订阅系统以支持苹果IAP交易记录存储和权益计算。

- 新增/pkg/iap/apple包处理JWS解析和产品映射
- 实现GET /products、POST /attach、POST /restore和GET /status接口
- 新增apple_iap_transactions表存储交易记录
- 更新文档说明配置方式和接口规范
- 移除旧的AppleIAP验证和通知处理逻辑
2025-12-13 20:54:50 -08:00

102 lines
3.1 KiB
Go

package apple
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
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/pkg/errors"
"gorm.io/gorm"
"github.com/google/uuid"
)
type AttachTransactionLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAttachTransactionLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AttachTransactionLogic {
return &AttachTransactionLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AttachTransactionLogic) Attach(req *types.AttachAppleTransactionRequest) (*types.AttachAppleTransactionResponse, error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok || u == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "invalid access")
}
txPayload, err := iapapple.ParseTransactionJWS(req.SignedTransactionJWS)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid jws")
}
pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData)
m, ok := pm.Items[txPayload.ProductId]
var duration int64
var tier string
var subscribeId int64
if ok {
duration = m.DurationDays
tier = m.Tier
subscribeId = m.SubscribeId
} else {
if req.DurationDays <= 0 || req.SubscribeId <= 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "unknown product")
}
duration = req.DurationDays
tier = req.Tier
subscribeId = req.SubscribeId
}
exp := iapapple.CalcExpire(txPayload.PurchaseDate, duration)
sum := sha256.Sum256([]byte(req.SignedTransactionJWS))
jwsHash := hex.EncodeToString(sum[:])
iapTx := &iapmodel.Transaction{
UserId: u.Id,
OriginalTransactionId: txPayload.OriginalTransactionId,
TransactionId: txPayload.TransactionId,
ProductId: txPayload.ProductId,
PurchaseAt: txPayload.PurchaseDate,
RevocationAt: txPayload.RevocationDate,
JWSHash: jwsHash,
}
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
if e := tx.Model(&iapmodel.Transaction{}).Create(iapTx).Error; e != nil {
return e
}
// 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,
}
return l.svcCtx.UserModel.InsertSubscribe(l.ctx, &userSub, tx)
})
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert error: %v", err.Error())
}
return &types.AttachAppleTransactionResponse{
ExpiresAt: exp.Unix(),
Tier: tier,
}, nil
}