Some checks failed
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支付订单 相关文档已更新,包含接入方案和实现细节
105 lines
4.0 KiB
Go
105 lines
4.0 KiB
Go
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
|
|
}
|
|
|