hi-server/internal/logic/notify/appleIAPNotifyLogic.go
shanshanzhong 3c6dd5058b
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 6m41s
feat(apple): 添加通过transaction_id附加苹果交易功能
新增通过transaction_id附加苹果交易的功能,包括:
1. 添加AttachAppleTransactionByIdRequest类型和对应路由
2. 实现AppleIAPConfig配置模型
3. 添加ServerAPI获取交易信息的实现
4. 优化JWS解析逻辑,增加cleanB64函数处理空格
5. 完善苹果通知处理逻辑的日志和注释
2025-12-15 22:35:33 -08:00

117 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package notify
import (
"context"
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/internal/svc"
iapapple "github.com/perfect-panel/server/pkg/iap/apple"
"github.com/perfect-panel/server/pkg/logger"
"gorm.io/gorm"
)
// AppleIAPNotifyLogic 用于处理 App Store Server Notifications V2 的苹果内购通知
// 负责JWS 验签、事务记录写入/撤销更新、订阅生命周期同步(续期/撤销等)
type AppleIAPNotifyLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewAppleIAPNotifyLogic 创建通知处理逻辑实例
// 参数:
// - ctx: 请求上下文
// - svcCtx: 服务上下文,包含 DB/Redis/配置 等
// 返回:
// - *AppleIAPNotifyLogic: 通知处理逻辑对象
func NewAppleIAPNotifyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AppleIAPNotifyLogic {
return &AppleIAPNotifyLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// Handle 处理苹果内购通知
// 流程:
// 1. 验签通知信封,解析得到交易 JWS 并再次验签;
// 2. 写入或更新事务记录(幂等按 OriginalTransactionId
// 3. 依据产品映射更新订阅到期时间或撤销状态;
// 4. 全流程关键节点输出详细中文日志,便于定位问题。
// 参数:
// - signedPayload: 通知信封的 JWS包含 data.signedTransactionInfo
// 返回:
// - error: 处理失败错误,成功返回 nil
func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error {
txPayload, ntype, err := iapapple.VerifyNotificationSignedPayload(signedPayload)
if err != nil {
// 验签失败,记录错误以便排查(通常为 JWS 格式/证书链问题)
l.Errorw("iap notify verify failed", logger.Field("error", err.Error()))
return err
}
// 验签通过,记录通知类型与关键交易标识
l.Infow("iap notify verified", logger.Field("type", ntype), logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
return l.svcCtx.DB.Transaction(func(db *gorm.DB) error {
var existing *iapmodel.Transaction
existing, _ = iapmodel.NewModel(l.svcCtx.DB, l.svcCtx.Redis).FindByOriginalId(l.ctx, txPayload.OriginalTransactionId)
if existing == nil || existing.Id == 0 {
// 首次出现该事务,写入记录
rec := &iapmodel.Transaction{
UserId: 0,
OriginalTransactionId: txPayload.OriginalTransactionId,
TransactionId: txPayload.TransactionId,
ProductId: txPayload.ProductId,
PurchaseAt: txPayload.PurchaseDate,
RevocationAt: txPayload.RevocationDate,
JWSHash: "",
}
if e := db.Model(&iapmodel.Transaction{}).Create(rec).Error; e != nil {
// 事务写入失败(唯一约束/字段问题),输出详细日志
l.Errorw("iap notify insert transaction error", logger.Field("error", e.Error()), logger.Field("productId", txPayload.ProductId), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
return e
}
} else {
if txPayload.RevocationDate != nil {
// 撤销场景:更新 revocation_at
if e := db.Model(&iapmodel.Transaction{}).
Where("original_transaction_id = ?", txPayload.OriginalTransactionId).
Update("revocation_at", txPayload.RevocationDate).Error; e != nil {
// 撤销更新失败,记录日志
l.Errorw("iap notify update revocation error", logger.Field("error", e.Error()), logger.Field("originalTransactionId", txPayload.OriginalTransactionId))
return e
}
}
}
pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData)
m := pm.Items[txPayload.ProductId]
// 若产品映射缺失,记录警告日志(不影响事务入库)
if m.DurationDays == 0 {
l.Errorw("iap notify product mapping missing", logger.Field("productId", txPayload.ProductId))
}
token := "iap:" + txPayload.OriginalTransactionId
sub, e := l.svcCtx.UserModel.FindOneSubscribeByToken(l.ctx, token)
if e == nil && sub != nil && sub.Id != 0 {
if txPayload.RevocationDate != nil {
// 撤销:订阅置为过期并记录完成时间
sub.Status = 3
t := *txPayload.RevocationDate
sub.FinishedAt = &t
sub.ExpireTime = t
} else if m.DurationDays > 0 {
// 正常:根据映射天数续期
exp := iapapple.CalcExpire(txPayload.PurchaseDate, m.DurationDays)
sub.ExpireTime = exp
sub.Status = 1
}
if e := l.svcCtx.UserModel.UpdateSubscribe(l.ctx, sub, db); e != nil {
// 订阅更新失败,记录日志
l.Errorw("iap notify update subscribe error", logger.Field("error", e.Error()), logger.Field("userSubscribeId", sub.Id))
return e
}
// 更新成功,输出订阅状态
l.Infow("iap notify updated subscribe", logger.Field("userSubscribeId", sub.Id), logger.Field("status", sub.Status))
}
return nil
})
}