hi-server/internal/logic/notify/appleIAPNotifyLogic.go
shanshanzhong 74f4a12422
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m55s
feat(contact): 添加联系信息提交功能
实现联系信息提交功能,包括:
1. 新增ContactRequest类型定义
2. 添加POST /contact路由
3. 实现联系信息提交处理逻辑
4. 通过Telegram发送联系信息通知
5. 在Telegram配置中添加GroupChatID字段
2025-12-21 19:32:23 -08:00

191 lines
6.6 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"
"encoding/json"
"strconv"
"strings"
iapmodel "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
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
}
}
}
var days int64
{
pid := strings.ToLower(txPayload.ProductId)
parts := strings.Split(pid, ".")
for i := len(parts) - 1; i >= 0; i-- {
p := parts[i]
var unit string
if strings.HasPrefix(p, "day") {
unit = "Day"
p = p[len("day"):]
} else if strings.HasPrefix(p, "month") {
unit = "Month"
p = p[len("month"):]
} else if strings.HasPrefix(p, "year") {
unit = "Year"
p = p[len("year"):]
}
if unit != "" {
digits := p
for j := 0; j < len(digits); j++ {
if digits[j] < '0' || digits[j] > '9' {
digits = digits[:j]
break
}
}
if q, e := strconv.ParseInt(digits, 10, 64); e == nil && q > 0 {
switch unit {
case "Day":
days = q
case "Month":
days = q * 30
case "Year":
days = q * 365
}
break
}
}
}
}
if days == 0 {
_, subs, e := l.svcCtx.SubscribeModel.FilterList(l.ctx, &subscribe.FilterParams{
Page: 1,
Size: 9999,
Show: true,
Sell: true,
DefaultLanguage: true,
})
if e == nil && len(subs) > 0 {
for _, item := range subs {
var discounts []types.SubscribeDiscount
if item.Discount != "" {
_ = json.Unmarshal([]byte(item.Discount), &discounts)
}
for _, d := range discounts {
if strings.Contains(strings.ToLower(txPayload.ProductId), strings.ToLower(item.UnitTime)) && d.Quantity > 0 {
// fallback not strict
if item.UnitTime == "Day" {
days = int64(d.Quantity)
} else if item.UnitTime == "Month" {
days = int64(d.Quantity) * 30
} else if item.UnitTime == "Year" {
days = int64(d.Quantity) * 365
}
break
}
}
if days > 0 {
break
}
}
}
}
if days == 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 days > 0 {
// 正常:根据映射天数续期
exp := iapapple.CalcExpire(txPayload.PurchaseDate, days)
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
})
}