同步历史版本代码
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
@@ -33,6 +34,7 @@ func NewCheckVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceConte
|
||||
|
||||
func (l *CheckVerificationCodeLogic) CheckVerificationCode(req *types.CheckVerificationCodeRequest) (resp *types.CheckVerificationCodeRespone, err error) {
|
||||
resp = &types.CheckVerificationCodeRespone{}
|
||||
req.Account = strings.ToLower(strings.TrimSpace(req.Account))
|
||||
if req.Method == authmethod.Email {
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.ParseVerifyType(req.Type), req.Account)
|
||||
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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 {
|
||||
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)
|
||||
if l.svcCtx.TelegramBot != nil {
|
||||
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
|
||||
}
|
||||
token := l.svcCtx.Config.Telegram.BotToken
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "telegram bot not initialized")
|
||||
}
|
||||
reqHttp, _ := http.NewRequest("GET", "https://api.telegram.org/bot"+token+"/sendMessage", nil)
|
||||
q := reqHttp.URL.Query()
|
||||
q.Add("chat_id", chatIDStr)
|
||||
q.Add("text", text)
|
||||
q.Add("parse_mode", "markdown")
|
||||
reqHttp.URL.RawQuery = q.Encode()
|
||||
resp, err := http.DefaultClient.Do(reqHttp)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "send telegram message failed: %v", err.Error())
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "send telegram message failed: http %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func escapeMarkdown(s string) string {
|
||||
return strings.ReplaceAll(s, "_", "\\_")
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
)
|
||||
|
||||
type GetDownloadLinkLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewGetDownloadLinkLogic 获取下载链接
|
||||
func NewGetDownloadLinkLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetDownloadLinkLogic {
|
||||
return &GetDownloadLinkLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDownloadLink 根据邀请码和平台动态生成下载链接
|
||||
// 生成的链接格式: https://{host}/v1/common/client/download/file/{platform}-{version}-ic_{invite_code}.{ext}
|
||||
// Nginx 会拦截此请求,将其映射到实际文件,并在 Content-Disposition 中设置带邀请码的文件名
|
||||
func (l *GetDownloadLinkLogic) GetDownloadLink(req *types.GetDownloadLinkRequest) (resp *types.GetDownloadLinkResponse, err error) {
|
||||
// 1. 获取站点域名 (数据库配置通常会覆盖文件配置)
|
||||
host := l.svcCtx.Config.Site.Host
|
||||
if host == "" {
|
||||
// 保底域名
|
||||
host = "api.airoport.co"
|
||||
}
|
||||
|
||||
// 2. 版本号 (后续可以从数据库或配置中读取)
|
||||
version := "1.0.0"
|
||||
|
||||
// 3. 根据平台确定文件扩展名
|
||||
var ext string
|
||||
switch req.Platform {
|
||||
case "windows":
|
||||
ext = ".exe"
|
||||
case "mac":
|
||||
ext = ".dmg"
|
||||
case "android":
|
||||
ext = ".apk"
|
||||
case "ios":
|
||||
ext = ".ipa"
|
||||
default:
|
||||
ext = ".bin"
|
||||
}
|
||||
|
||||
// 4. 构建文件名: Hi快VPN-平台-版本号[-ic_邀请码].扩展名
|
||||
const AppNamePrefix = "Hi快VPN"
|
||||
var filename string
|
||||
if req.InviteCode != "" {
|
||||
filename = fmt.Sprintf("%s-%s-%s-ic-%s%s", AppNamePrefix, req.Platform, version, req.InviteCode, ext)
|
||||
} else {
|
||||
filename = fmt.Sprintf("%s-%s-%s%s", AppNamePrefix, req.Platform, version, ext)
|
||||
}
|
||||
|
||||
// 5. 构建完整 URL (Nginx 会拦截此路径进行虚拟更名处理)
|
||||
url := fmt.Sprintf("https://%s/v1/common/client/download/file/%s", host, filename)
|
||||
|
||||
return &types.GetDownloadLinkResponse{
|
||||
Url: url,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
logmessage "github.com/perfect-panel/server/internal/model/logmessage"
|
||||
"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 ReportLogMessageLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewReportLogMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ReportLogMessageLogic {
|
||||
return &ReportLogMessageLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx }
|
||||
}
|
||||
|
||||
func (l *ReportLogMessageLogic) ReportLogMessage(req *types.ReportLogMessageRequest, c *gin.Context) (resp *types.ReportLogMessageResponse, err error) {
|
||||
ip := clientIP(c)
|
||||
ua := c.GetHeader("User-Agent")
|
||||
locale := c.GetHeader("Accept-Language")
|
||||
|
||||
// 简单限流:设备ID优先,其次IP
|
||||
limitKey := "logmsg:" + strings.TrimSpace(req.DeviceId)
|
||||
if limitKey == "logmsg:" { limitKey = "logmsg:" + ip }
|
||||
count, _ := l.svcCtx.Redis.Incr(l.ctx, limitKey).Result()
|
||||
if count == 1 { _ = l.svcCtx.Redis.Expire(l.ctx, limitKey, 60*time.Second).Err() }
|
||||
if count > 120 { // 每分钟最多120条
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TooManyRequests), "too many reports")
|
||||
}
|
||||
|
||||
// 指纹生成
|
||||
h := sha256.New()
|
||||
h.Write([]byte(strings.Join([]string{req.Message, req.Stack, req.ErrorCode, req.AppVersion, req.Platform}, "|")))
|
||||
digest := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
var ctxStr string
|
||||
if req.Context != nil {
|
||||
if b, e := json.Marshal(req.Context); e == nil {
|
||||
ctxStr = string(b)
|
||||
}
|
||||
}
|
||||
var occurredAt *time.Time
|
||||
if req.OccurredAt > 0 {
|
||||
t := time.UnixMilli(req.OccurredAt)
|
||||
occurredAt = &t
|
||||
}
|
||||
|
||||
var userIdPtr *int64
|
||||
if req.UserId > 0 { userIdPtr = &req.UserId }
|
||||
|
||||
row := &logmessage.LogMessage{
|
||||
Platform: req.Platform,
|
||||
AppVersion: req.AppVersion,
|
||||
OsName: req.OsName,
|
||||
OsVersion: req.OsVersion,
|
||||
DeviceId: req.DeviceId,
|
||||
UserId: userIdPtr,
|
||||
SessionId: req.SessionId,
|
||||
Level: req.Level,
|
||||
ErrorCode: req.ErrorCode,
|
||||
Message: safeTruncate(req.Message, 1024*64),
|
||||
Stack: safeTruncate(req.Stack, 1024*1024),
|
||||
Context: ctxStr,
|
||||
ClientIP: ip,
|
||||
UserAgent: safeTruncate(ua, 255),
|
||||
Locale: safeTruncate(locale, 16),
|
||||
Digest: digest,
|
||||
OccurredAt: occurredAt,
|
||||
}
|
||||
|
||||
if err = l.svcCtx.LogMessageModel.Insert(l.ctx, row); err != nil {
|
||||
// 唯一指纹冲突时尝试查询已有记录返回ID
|
||||
ex, _, findErr := l.svcCtx.LogMessageModel.Filter(l.ctx, &logmessage.FilterParams{ Keyword: req.Message, Page: 1, Size: 1 })
|
||||
if findErr == nil && len(ex) > 0 {
|
||||
return &types.ReportLogMessageResponse{ Id: ex[0].Id }, nil
|
||||
}
|
||||
l.Errorf("[ReportLogMessage] insert error: %v", err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "insert log_message failed: %v", err)
|
||||
}
|
||||
return &types.ReportLogMessageResponse{ Id: row.Id }, nil
|
||||
}
|
||||
|
||||
func safeTruncate(s string, n int) string {
|
||||
if len(s) <= n { return s }
|
||||
return s[:n]
|
||||
}
|
||||
|
||||
func clientIP(c *gin.Context) string {
|
||||
ip := c.ClientIP()
|
||||
if ip != "" { return ip }
|
||||
host, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
|
||||
if err == nil && host != "" { return host }
|
||||
return ""
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
@@ -53,6 +54,7 @@ func NewSendEmailCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Sen
|
||||
}
|
||||
|
||||
func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *types.SendCodeResponse, err error) {
|
||||
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
|
||||
// Check if there is Redis in the code
|
||||
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.ParseVerifyType(req.Type), req.Email)
|
||||
// Check if the limit is exceeded of current request
|
||||
@@ -89,11 +91,17 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
||||
taskPayload.Type = queue.EmailTypeVerify
|
||||
taskPayload.Email = req.Email
|
||||
taskPayload.Subject = "Verification code"
|
||||
|
||||
expireTime := l.svcCtx.Config.VerifyCode.ExpireTime
|
||||
if expireTime == 0 {
|
||||
expireTime = 900
|
||||
}
|
||||
expireMinutes := expireTime / 60
|
||||
taskPayload.Content = map[string]interface{}{
|
||||
"Type": req.Type,
|
||||
"SiteLogo": l.svcCtx.Config.Site.SiteLogo,
|
||||
"SiteName": l.svcCtx.Config.Site.SiteName,
|
||||
"Expire": 5,
|
||||
"Expire": expireMinutes,
|
||||
"Code": code,
|
||||
}
|
||||
// Save to Redis
|
||||
@@ -103,7 +111,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
||||
}
|
||||
// Marshal the payload
|
||||
val, _ := json.Marshal(payload)
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*IntervalTime*5).Err(); err != nil {
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, cacheKey, string(val), time.Second*time.Duration(expireTime)).Err(); err != nil {
|
||||
l.Errorw("[SendEmailCode]: Redis Error", logger.Field("error", err.Error()), logger.Field("cacheKey", cacheKey))
|
||||
return nil, errors.Wrap(xerr.NewErrCode(xerr.ERROR), "Failed to set verification code")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user