feat(contact): 添加联系信息提交功能
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 5m55s

实现联系信息提交功能,包括:
1. 新增ContactRequest类型定义
2. 添加POST /contact路由
3. 实现联系信息提交处理逻辑
4. 通过Telegram发送联系信息通知
5. 在Telegram配置中添加GroupChatID字段
This commit is contained in:
shanshanzhong 2025-12-21 19:32:23 -08:00
parent 2fdc9c8127
commit 74f4a12422
8 changed files with 207 additions and 12 deletions

View File

@ -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)
}

View File

@ -29,12 +29,16 @@ func Telegram(svc *svc.ServiceContext) {
return
}
if tgConfig.BotToken == "" {
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

View File

@ -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:""`
}

View File

@ -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)
}
}

View File

@ -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))

View File

@ -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, "_", "\\_")
}

View File

@ -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
}

View File

@ -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"`
}