diff --git a/apis/common.api b/apis/common.api index e965d5e..a196945 100644 --- a/apis/common.api +++ b/apis/common.api @@ -87,6 +87,12 @@ type ( Total int64 `json:"total"` List []SubscribeClient `json:"list"` } + ContactRequest { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + OtherContact string `json:"other_contact,optional"` + Notes string `json:"notes,optional"` + } ) @server ( @@ -99,6 +105,10 @@ service ppanel { @handler GetGlobalConfig get /site/config returns (GetGlobalConfigResponse) + @doc "Submit contact info" + @handler SubmitContact + post /contact (ContactRequest) + @doc "Get Tos Content" @handler GetTos get /site/tos returns (GetTosResponse) @@ -131,4 +141,3 @@ service ppanel { @handler GetClient get /client returns (GetSubscribeClientResponse) } - diff --git a/initialize/telegram.go b/initialize/telegram.go index 56c7086..5d8fc9d 100644 --- a/initialize/telegram.go +++ b/initialize/telegram.go @@ -29,12 +29,16 @@ func Telegram(svc *svc.ServiceContext) { return } - if tgConfig.BotToken == "" { - logger.Debug("[Init Telegram Config] Telegram Token is empty") - return + usedToken := tgConfig.BotToken + if usedToken == "" { + usedToken = svc.Config.Telegram.BotToken + if usedToken == "" { + logger.Debug("[Init Telegram Config] Telegram Token is empty") + return + } } - bot, err := tgbotapi.NewBotAPI(tg.BotToken) + bot, err := tgbotapi.NewBotAPI(usedToken) if err != nil { logger.Error("[Init Telegram Config] New Bot API Error: ", logger.Field("error", err.Error())) return @@ -55,7 +59,7 @@ func Telegram(svc *svc.ServiceContext) { } }() } else { - wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", tgConfig.WebHookDomain, tool.Md5Encode(tgConfig.BotToken, false))) + wh, err := tgbotapi.NewWebhook(fmt.Sprintf("%s/v1/telegram/webhook?secret=%s", tgConfig.WebHookDomain, tool.Md5Encode(usedToken, false))) if err != nil { logger.Errorf("[Init Telegram Config] New Webhook Error: %s", err.Error()) return @@ -74,6 +78,7 @@ func Telegram(svc *svc.ServiceContext) { } svc.Config.Telegram.BotID = user.ID svc.Config.Telegram.BotName = user.UserName + svc.Config.Telegram.BotToken = usedToken svc.Config.Telegram.EnableNotify = tg.EnableNotify svc.Config.Telegram.WebHookDomain = tg.WebHookDomain svc.TelegramBot = bot diff --git a/internal/config/config.go b/internal/config/config.go index b75d9d1..70ce066 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -212,6 +212,7 @@ type Telegram struct { BotID int64 `yaml:"BotID" default:""` BotName string `yaml:"BotName" default:""` BotToken string `yaml:"BotToken" default:""` + GroupChatID string `yaml:"GroupChatID" default:""` EnableNotify bool `yaml:"EnableNotify" default:"false"` WebHookDomain string `yaml:"WebHookDomain" default:""` } diff --git a/internal/handler/common/contactHandler.go b/internal/handler/common/contactHandler.go new file mode 100644 index 0000000..d6e4aea --- /dev/null +++ b/internal/handler/common/contactHandler.go @@ -0,0 +1,25 @@ +package common + +import ( + "github.com/gin-gonic/gin" + "github.com/perfect-panel/server/internal/logic/common" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/result" +) + +func SubmitContactHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) { + return func(c *gin.Context) { + var req types.ContactRequest + _ = c.ShouldBindJSON(&req) + validateErr := svcCtx.Validate(&req) + if validateErr != nil { + result.ParamErrorResult(c, validateErr) + return + } + l := common.NewContactLogic(c.Request.Context(), svcCtx) + err := l.SubmitContact(&req) + result.HttpResult(c, nil, err) + } +} + diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 157c591..428ada5 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -642,6 +642,9 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { // Get Client commonGroupRouter.GET("/client", common.GetClientHandler(serverCtx)) + // Submit contact info + commonGroupRouter.POST("/contact", common.SubmitContactHandler(serverCtx)) + // Get verification code commonGroupRouter.POST("/send_code", common.SendEmailCodeHandler(serverCtx)) diff --git a/internal/logic/common/contactLogic.go b/internal/logic/common/contactLogic.go new file mode 100644 index 0000000..7695254 --- /dev/null +++ b/internal/logic/common/contactLogic.go @@ -0,0 +1,70 @@ +package common + +import ( + "context" + "fmt" + "strconv" + "strings" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "github.com/perfect-panel/server/internal/svc" + "github.com/perfect-panel/server/internal/types" + "github.com/perfect-panel/server/pkg/logger" + "github.com/perfect-panel/server/pkg/xerr" + "github.com/pkg/errors" +) + +type ContactLogic struct { + logger.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewContactLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ContactLogic { + return &ContactLogic{ + Logger: logger.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ContactLogic) SubmitContact(req *types.ContactRequest) error { + if l.svcCtx.TelegramBot == nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "telegram bot not initialized") + } + chatIDStr := l.svcCtx.Config.Telegram.GroupChatID + if chatIDStr == "" { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "telegram group chat id not configured") + } + chatID, err := strconv.ParseInt(chatIDStr, 10, 64) + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "invalid group chat id: %v", err.Error()) + } + + name := escapeMarkdown(req.Name) + email := escapeMarkdown(req.Email) + other := req.OtherContact + if strings.TrimSpace(other) == "" { + other = "无" + } + other = escapeMarkdown(other) + notes := req.Notes + if strings.TrimSpace(notes) == "" { + notes = "无" + } + notes = escapeMarkdown(notes) + + text := fmt.Sprintf("新的联系/合作信息\n称呼:%s\n邮箱:%s\n其他联系方式:%s\n优势/备注:%s", name, email, other, notes) + msg := tgbotapi.NewMessage(chatID, text) + msg.ParseMode = "markdown" + _, err = l.svcCtx.TelegramBot.Send(msg) + if err != nil { + l.Errorw("send telegram message failed", logger.Field("error", err.Error())) + return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "send telegram message failed: %v", err.Error()) + } + return nil +} + +func escapeMarkdown(s string) string { + return strings.ReplaceAll(s, "_", "\\_") +} diff --git a/internal/logic/notify/appleIAPNotifyLogic.go b/internal/logic/notify/appleIAPNotifyLogic.go index 165545a..0770db8 100644 --- a/internal/logic/notify/appleIAPNotifyLogic.go +++ b/internal/logic/notify/appleIAPNotifyLogic.go @@ -2,9 +2,14 @@ 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" @@ -82,10 +87,79 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error { } } } - pm, _ := iapapple.ParseProductMap(l.svcCtx.Config.Site.CustomData) - m := pm.Items[txPayload.ProductId] - // 若产品映射缺失,记录警告日志(不影响事务入库) - if m.DurationDays == 0 { + 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 @@ -97,9 +171,9 @@ func (l *AppleIAPNotifyLogic) Handle(signedPayload string) error { t := *txPayload.RevocationDate sub.FinishedAt = &t sub.ExpireTime = t - } else if m.DurationDays > 0 { + } else if days > 0 { // 正常:根据映射天数续期 - exp := iapapple.CalcExpire(txPayload.PurchaseDate, m.DurationDays) + exp := iapapple.CalcExpire(txPayload.PurchaseDate, days) sub.ExpireTime = exp sub.Status = 1 } diff --git a/internal/types/contact.go b/internal/types/contact.go new file mode 100644 index 0000000..696febc --- /dev/null +++ b/internal/types/contact.go @@ -0,0 +1,8 @@ +package types + +type ContactRequest struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"required,email"` + OtherContact string `json:"other_contact,omitempty"` + Notes string `json:"notes,omitempty"` +}