feat(iap/apple): 实现苹果IAP非续期订阅功能
Build docker and publish / build (20.15.1) (push) Successful in 6m37s

新增苹果IAP相关接口与逻辑,包括产品列表查询、交易绑定、状态查询和恢复购买功能。移除旧的IAP验证逻辑,重构订阅系统以支持苹果IAP交易记录存储和权益计算。

- 新增/pkg/iap/apple包处理JWS解析和产品映射
- 实现GET /products、POST /attach、POST /restore和GET /status接口
- 新增apple_iap_transactions表存储交易记录
- 更新文档说明配置方式和接口规范
- 移除旧的AppleIAP验证和通知处理逻辑
This commit is contained in:
2025-12-13 20:54:50 -08:00
parent a80d6af035
commit 62186ca672
41 changed files with 1715 additions and 474 deletions
@@ -1,134 +0,0 @@
package notify
import (
"context"
"encoding/json"
"io"
"net/http"
"github.com/hibiken/asynq"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/appleiap"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/payment"
queueType "github.com/perfect-panel/server/queue/types"
)
// AppleIAPNotifyLogic 处理 Apple Server Notifications v2 的逻辑
// 功能: 验签与事件解析(此处提供最小骨架),将续期/初购事件转换为订单并入队赋权
// 参数: HTTP 请求
// 返回: 错误信息
type AppleIAPNotifyLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewAppleIAPNotifyLogic 创建逻辑实例
// 参数: 上下文, 服务上下文
// 返回: 逻辑指针
func NewAppleIAPNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppleIAPNotifyLogic {
return &AppleIAPNotifyLogic{Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
}
// AppleNotification 简化的通知结构(骨架)
type rawPayload struct {
SignedPayload string `json:"signedPayload"`
}
type transactionInfo struct {
OriginalTransactionId string `json:"originalTransactionId"`
TransactionId string `json:"transactionId"`
ProductId string `json:"productId"`
}
// Handle 处理通知
// 参数: *http.Request
// 返回: error
func (l *AppleIAPNotifyLogic) Handle(r *http.Request) error {
body, _ := io.ReadAll(r.Body)
var rp rawPayload
if err := json.Unmarshal(body, &rp); err != nil {
l.Errorw("[AppleIAP] Unmarshal request failed", logger.Field("error", err.Error()))
return err
}
claims, env, err := appleiap.VerifyAutoEnv(rp.SignedPayload)
if err != nil {
l.Errorw("[AppleIAP] Verify payload failed", logger.Field("error", err.Error()))
return err
}
t, _ := claims["notificationType"].(string)
data, _ := claims["data"].(map[string]interface{})
sti, _ := data["signedTransactionInfo"].(string)
txClaims, err := appleiap.VerifyWithEnv(env, sti)
if err != nil {
l.Errorw("[AppleIAP] Verify transaction failed", logger.Field("error", err.Error()))
return err
}
b, _ := json.Marshal(txClaims)
var tx transactionInfo
_ = json.Unmarshal(b, &tx)
switch t {
case "INITIAL_BUY":
return l.processInitialBuy(env, tx)
case "DID_RENEW":
return l.processRenew(env, tx)
default:
return nil
}
}
// createPaidOrderAndEnqueue 创建已支付订单并入队赋权/续费
// 参数: AppleNotification, 订单类型
// 返回: error
func (l *AppleIAPNotifyLogic) processInitialBuy(env string, tx transactionInfo) error {
if tx.OriginalTransactionId == "" || tx.TransactionId == "" {
return nil
}
// if order already exists, ignore
if oi, err := l.svcCtx.OrderModel.FindOneByTradeNo(l.ctx, tx.OriginalTransactionId); err == nil && oi != nil {
return nil
}
return nil
}
func (l *AppleIAPNotifyLogic) processRenew(env string, tx transactionInfo) error {
if tx.OriginalTransactionId == "" || tx.TransactionId == "" {
return nil
}
oi, err := l.svcCtx.OrderModel.FindOneByTradeNo(l.ctx, tx.OriginalTransactionId)
if err != nil || oi == nil {
return nil
}
o := &order.Order{
UserId: oi.UserId,
OrderNo: tx.TransactionId,
Type: 2,
Quantity: 1,
Price: 0,
Amount: 0,
Discount: 0,
Coupon: "",
CouponDiscount: 0,
PaymentId: 0,
Method: payment.AppleIAP.String(),
FeeAmount: 0,
Status: 2,
IsNew: false,
SubscribeId: oi.SubscribeId,
TradeNo: tx.OriginalTransactionId,
SubscribeToken: oi.SubscribeToken,
}
if err := l.svcCtx.OrderModel.Insert(l.ctx, o); err != nil {
return err
}
payload := queueType.ForthwithActivateOrderPayload{OrderNo: o.OrderNo}
bytes, _ := json.Marshal(payload)
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
if _, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task); err != nil {
return err
}
return nil
}