feat(支付): 新增Apple IAP支付支持
Build docker and publish / build (20.15.1) (push) Has been cancelled

实现Apple应用内购支付功能,包括:
1. 新增AppleIAP和ApplePay支付平台枚举
2. 添加IAP验证接口/v1/public/iap/verify处理初购验证
3. 实现Apple服务器通知处理逻辑/v1/iap/notifications
4. 新增JWS验签和JWKS公钥缓存功能
5. 复用现有订单系统处理IAP支付订单

相关文档已更新,包含接入方案和实现细节
This commit is contained in:
2025-12-09 00:53:25 -08:00
parent 4b6fcb338e
commit d95911d6bd
15 changed files with 654 additions and 34 deletions
+104
View File
@@ -0,0 +1,104 @@
package iap
import (
"context"
"encoding/json"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/order"
"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"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/payment"
"github.com/perfect-panel/server/pkg/tool"
"github.com/perfect-panel/server/pkg/xerr"
queueType "github.com/perfect-panel/server/queue/types"
"github.com/pkg/errors"
)
// VerifyLogic 处理 IAP 初购验证并生成已支付订阅订单
// 功能: 校验用户与订阅参数, 创建已支付订单并触发赋权队列
// 参数: IAPVerifyRequest
// 返回: IAPVerifyResponse 与错误
type VerifyLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewVerifyLogic 创建 VerifyLogic
// 参数: 上下文, 服务上下文
// 返回: VerifyLogic 指针
func NewVerifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *VerifyLogic {
return &VerifyLogic{Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// Verify 执行 IAP 初购验证并创建订单
// 参数: IAPVerifyRequest 包含 original_transaction_id 与 subscribe_id
// 返回: IAPVerifyResponse 包含 order_no
func (l *VerifyLogic) Verify(req *types.IAPVerifyRequest) (resp *types.IAPVerifyResponse, err error) {
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
logger.Error("current user is not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
if req.SubscribeId <= 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid subscribe_id")
}
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, req.SubscribeId)
if err != nil {
l.Errorw("[IAP Verify] Find subscribe failed", logger.Field("error", err.Error()), logger.Field("subscribe_id", req.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
if sub.Sell != nil && !*sub.Sell {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "subscribe not sell")
}
isNew, err := l.svcCtx.OrderModel.IsUserEligibleForNewOrder(l.ctx, u.Id)
if err != nil {
l.Errorw("[IAP Verify] Query user new purchase failed", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user error: %v", err.Error())
}
orderInfo := &order.Order{
UserId: u.Id,
OrderNo: tool.GenerateTradeNo(),
Type: 1,
Quantity: 1,
Price: sub.UnitPrice,
Amount: 0,
Discount: 0,
Coupon: "",
CouponDiscount: 0,
PaymentId: 0,
Method: payment.AppleIAP.String(),
FeeAmount: 0,
Status: 2,
IsNew: isNew,
SubscribeId: req.SubscribeId,
TradeNo: req.OriginalTransactionId,
}
if err = l.svcCtx.OrderModel.Insert(l.ctx, orderInfo); err != nil {
l.Errorw("[IAP Verify] Insert order failed", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert order error: %v", err.Error())
}
payload := queueType.ForthwithActivateOrderPayload{OrderNo: orderInfo.OrderNo}
bytes, err := json.Marshal(payload)
if err != nil {
l.Errorw("[IAP Verify] Marshal payload failed", logger.Field("error", err.Error()))
return nil, err
}
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
if _, err = l.svcCtx.Queue.EnqueueContext(l.ctx, task); err != nil {
l.Errorw("[IAP Verify] Enqueue activation failed", logger.Field("error", err.Error()))
return nil, err
}
return &types.IAPVerifyResponse{OrderNo: orderInfo.OrderNo}, nil
}
@@ -86,7 +86,18 @@ func (l *PurchaseCheckoutLogic) PurchaseCheckout(req *types.CheckoutOrderRequest
case paymentPlatform.Stripe:
// Process Stripe payment - creates payment sheet for client-side processing
stripePayment, err := l.stripePayment(paymentConfig.Config, orderInfo, "")
stripePayment, err := l.stripePayment(paymentConfig.Config, orderInfo, "", "")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "stripePayment error: %v", err.Error())
}
resp = &types.CheckoutOrderResponse{
Type: "stripe", // Client should use Stripe SDK
Stripe: stripePayment,
}
case paymentPlatform.ApplePay:
// Process Apple Pay payment - uses Stripe with 'card' method
stripePayment, err := l.stripePayment(paymentConfig.Config, orderInfo, "", "card")
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "stripePayment error: %v", err.Error())
}
@@ -208,7 +219,7 @@ func (l *PurchaseCheckoutLogic) alipayF2fPayment(pay *payment.Payment, info *ord
// stripePayment processes Stripe payment by creating a payment sheet
// It supports various payment methods including WeChat Pay and Alipay through Stripe
func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string) (*types.StripePayment, error) {
func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order, identifier string, forceMethod string) (*types.StripePayment, error) {
// Parse Stripe configuration from payment settings
stripeConfig := &payment.StripeConfig{}
@@ -217,6 +228,10 @@ func (l *PurchaseCheckoutLogic) stripePayment(config string, info *order.Order,
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "Unmarshal error: %s", err.Error())
}
if forceMethod != "" {
stripeConfig.Payment = forceMethod
}
// Initialize Stripe client with API credentials
client := stripe.NewClient(stripe.Config{
SecretKey: stripeConfig.SecretKey,