refactor: 更新项目引用路径从perfect-panel/ppanel-server到perfect-panel/server

feat: 添加版本和构建时间变量
fix: 修正短信队列类型注释错误
style: 清理未使用的代码和测试文件
docs: 更新安装文档中的下载链接
chore: 迁移数据库脚本添加日志和订阅配置
This commit is contained in:
2025-10-13 01:33:03 -07:00
parent 393b42f35a
commit c582087c0f
974 changed files with 23609 additions and 23398 deletions
+172 -179
View File
@@ -2,23 +2,23 @@ package subscribe
import (
"fmt"
"net/url"
"strings"
"time"
"github.com/perfect-panel/ppanel-server/pkg/adapter"
"github.com/perfect-panel/ppanel-server/pkg/adapter/shadowrocket"
"github.com/perfect-panel/ppanel-server/pkg/adapter/surfboard"
"github.com/perfect-panel/server/adapter"
"github.com/perfect-panel/server/internal/model/client"
"github.com/perfect-panel/server/internal/model/log"
"github.com/perfect-panel/server/internal/model/node"
"github.com/perfect-panel/ppanel-server/internal/model/server"
"github.com/perfect-panel/ppanel-server/internal/model/user"
"github.com/perfect-panel/server/internal/model/user"
"github.com/gin-gonic/gin"
"github.com/perfect-panel/ppanel-server/internal/svc"
"github.com/perfect-panel/ppanel-server/internal/types"
"github.com/perfect-panel/ppanel-server/pkg/logger"
"github.com/perfect-panel/ppanel-server/pkg/tool"
"github.com/perfect-panel/ppanel-server/pkg/xerr"
"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/tool"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
@@ -37,37 +37,123 @@ func NewSubscribeLogic(ctx *gin.Context, svc *svc.ServiceContext) *SubscribeLogi
}
}
func (l *SubscribeLogic) Generate(req *types.SubscribeRequest) (*types.SubscribeResponse, error) {
userSub, err := l.getUserSubscribe(req.Token)
func (l *SubscribeLogic) Handler(req *types.SubscribeRequest) (resp *types.SubscribeResponse, err error) {
// query client list
clients, err := l.svc.ClientModel.List(l.ctx.Request.Context())
if err != nil {
l.Errorw("[SubscribeLogic] Query client list failed", logger.Field("error", err.Error()))
return nil, err
}
userAgent := strings.ToLower(l.ctx.Request.UserAgent())
var targetApp, defaultApp *client.SubscribeApplication
for _, item := range clients {
u := strings.ToLower(item.UserAgent)
if item.IsDefault {
defaultApp = item
}
if strings.Contains(userAgent, u) {
// Special handling for Stash
if strings.Contains(userAgent, "stash") && !strings.Contains(u, "stash") {
continue
}
targetApp = item
break
}
}
if targetApp == nil {
l.Debugf("[SubscribeLogic] No matching client found", logger.Field("userAgent", userAgent))
if defaultApp == nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "No matching client found for user agent: %s", userAgent)
}
targetApp = defaultApp
}
// Find user subscribe by token
userSubscribe, err := l.getUserSubscribe(req.Token)
if err != nil {
l.Errorw("[SubscribeLogic] Get user subscribe failed", logger.Field("error", err.Error()), logger.Field("token", req.Token))
return nil, err
}
var subscribeStatus = false
defer func() {
l.logSubscribeActivity(subscribeStatus, userSub, req)
l.logSubscribeActivity(subscribeStatus, userSubscribe, req)
}()
// find subscribe info
subscribeInfo, err := l.svc.SubscribeModel.FindOne(l.ctx.Request.Context(), userSubscribe.SubscribeId)
if err != nil {
l.Errorw("[SubscribeLogic] Find subscribe info failed", logger.Field("error", err.Error()), logger.Field("subscribeId", userSubscribe.SubscribeId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "Find subscribe info failed: %v", err.Error())
}
servers, err := l.getServers(userSub)
// Find server list by user subscribe
servers, err := l.getServers(userSubscribe)
if err != nil {
return nil, err
}
a := adapter.NewAdapter(
targetApp.SubscribeTemplate,
adapter.WithServers(servers),
adapter.WithSiteName(l.svc.Config.Site.SiteName),
adapter.WithSubscribeName(subscribeInfo.Name),
adapter.WithOutputFormat(targetApp.OutputFormat),
adapter.WithUserInfo(adapter.User{
Password: userSubscribe.UUID,
ExpiredAt: userSubscribe.ExpireTime,
Download: userSubscribe.Download,
Upload: userSubscribe.Upload,
Traffic: userSubscribe.Traffic,
SubscribeURL: l.getSubscribeV2URL(req.Token),
}),
)
rules, err := l.getRules()
// Get client config
adapterClient, err := a.Client()
if err != nil {
return nil, err
l.Errorw("[SubscribeLogic] Client error", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(500), "Client error: %v", err.Error())
}
bytes, err := adapterClient.Build()
if err != nil {
l.Errorw("[SubscribeLogic] Build client config failed", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(500), "Build client config failed: %v", err.Error())
}
resp, headerInfo, err := l.buildClientConfig(req, userSub, servers, rules)
if err != nil {
return nil, err
var formats = []string{"json", "yaml", "conf"}
for _, format := range formats {
if format == strings.ToLower(targetApp.OutputFormat) {
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.%s", url.QueryEscape(l.svc.Config.Site.SiteName), format))
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
}
resp = &types.SubscribeResponse{
Config: bytes,
Header: fmt.Sprintf(
"upload=%d;download=%d;total=%d;expire=%d",
userSubscribe.Upload, userSubscribe.Download, userSubscribe.Traffic, userSubscribe.ExpireTime.Unix(),
),
}
subscribeStatus = true
return &types.SubscribeResponse{
Config: resp,
Header: headerInfo,
}, nil
return
}
func (l *SubscribeLogic) getSubscribeV2URL(token string) string {
if l.svc.Config.Subscribe.PanDomain {
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
}
if l.svc.Config.Subscribe.SubscribeDomain != "" {
domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
return fmt.Sprintf("https://%s%s?token=%s", domains[0], l.svc.Config.Subscribe.SubscribePath, token)
}
return fmt.Sprintf("https://%s%s?token=%s&", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token)
}
func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error) {
@@ -77,10 +163,11 @@ func (l *SubscribeLogic) getUserSubscribe(token string) (*user.Subscribe, error)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe error: %v", err.Error())
}
if userSub.Status != 1 {
l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available")
}
// Ignore expiration check
//if userSub.Status > 1 {
// l.Infow("[Generate Subscribe]subscribe is not available", logger.Field("status", int(userSub.Status)), logger.Field("token", token))
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.SubscribeNotAvailable), "subscribe is not available")
//}
return userSub, nil
}
@@ -90,19 +177,27 @@ func (l *SubscribeLogic) logSubscribeActivity(subscribeStatus bool, userSub *use
return
}
err := l.svc.UserModel.InsertSubscribeLog(l.ctx.Request.Context(), &user.SubscribeLog{
UserId: userSub.UserId,
UserSubscribeId: userSub.Id,
subscribeLog := log.Subscribe{
Token: req.Token,
IP: l.ctx.ClientIP(),
UserAgent: l.ctx.Request.UserAgent(),
UserAgent: req.UA,
ClientIP: l.ctx.ClientIP(),
UserSubscribeId: userSub.Id,
}
content, _ := subscribeLog.Marshal()
err := l.svc.LogModel.Insert(l.ctx.Request.Context(), &log.SystemLog{
Type: log.TypeSubscribe.Uint8(),
ObjectID: userSub.UserId, // log user id
Date: time.Now().Format(time.DateOnly),
Content: string(content),
})
if err != nil {
l.Errorw("[Generate Subscribe]insert subscribe log error: %v", logger.Field("error", err.Error()))
}
}
func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server, error) {
func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*node.Node, error) {
if l.isSubscriptionExpired(userSub) {
return l.createExpiredServers(), nil
}
@@ -113,44 +208,66 @@ func (l *SubscribeLogic) getServers(userSub *user.Subscribe) ([]*server.Server,
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find subscribe details error: %v", err.Error())
}
serverIds := tool.StringToInt64Slice(subDetails.Server)
groupIds := tool.StringToInt64Slice(subDetails.ServerGroup)
nodeIds := tool.StringToInt64Slice(subDetails.Nodes)
tags := strings.Split(subDetails.NodeTags, ",")
l.Debugf("[Generate Subscribe]nodes: %v, NodeTags: %v", nodeIds, tags)
enable := true
_, nodes, err := l.svc.NodeModel.FilterNodeList(l.ctx.Request.Context(), &node.FilterNodeParams{
Page: 1,
Size: 1000,
NodeId: nodeIds,
Tag: tool.RemoveDuplicateElements(tags...),
Preload: true,
Enabled: &enable, // Only get enabled nodes
})
l.Debugf("[Query Subscribe]found servers: %v", len(nodes))
servers, err := l.svc.ServerModel.FindServerDetailByGroupIdsAndIds(l.ctx.Request.Context(), groupIds, serverIds)
if err != nil {
l.Errorw("[Generate Subscribe]find server details error: %v", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find server details error: %v", err.Error())
}
return servers, nil
logger.Debugf("[Generate Subscribe]found servers: %v", len(nodes))
return nodes, nil
}
func (l *SubscribeLogic) isSubscriptionExpired(userSub *user.Subscribe) bool {
return userSub.ExpireTime.Unix() < time.Now().Unix() && userSub.ExpireTime.Unix() != 0
}
func (l *SubscribeLogic) createExpiredServers() []*server.Server {
func (l *SubscribeLogic) createExpiredServers() []*node.Node {
enable := true
host := l.getFirstHostLine()
return []*server.Server{
return []*node.Node{
{
Name: "Subscribe Expired",
ServerAddr: "127.0.0.1",
RelayMode: "none",
Protocol: "shadowsocks",
Config: "{\"method\":\"aes-256-gcm\",\"port\":1}",
Enable: &enable,
Sort: 0,
Name: "Subscribe Expired",
Tags: "",
Port: 18080,
Address: "127.0.0.1",
Server: &node.Server{
Id: 1,
Name: "Subscribe Expired",
Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]",
},
Protocol: "shadowsocks",
Enabled: &enable,
},
{
Name: host,
ServerAddr: "127.0.0.1",
RelayMode: "none",
Protocol: "shadowsocks",
Config: "{\"method\":\"aes-256-gcm\",\"port\":1}",
Enable: &enable,
Sort: 0,
Name: host,
Tags: "",
Port: 18080,
Address: "127.0.0.1",
Server: &node.Server{
Id: 1,
Name: "Subscribe Expired",
Protocols: "[{\"type\":\"shadowsocks\",\"cipher\":\"aes-256-gcm\",\"port\":1}]",
},
Protocol: "shadowsocks",
Enabled: &enable,
},
}
}
@@ -163,127 +280,3 @@ func (l *SubscribeLogic) getFirstHostLine() string {
}
return host
}
func (l *SubscribeLogic) getRules() ([]*server.RuleGroup, error) {
rules, err := l.svc.ServerModel.QueryAllRuleGroup(l.ctx)
if err != nil {
l.Errorw("[Generate Subscribe]find rule group error: %v", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find rule group error: %v", err.Error())
}
return rules, nil
}
func (l *SubscribeLogic) buildClientConfig(req *types.SubscribeRequest, userSub *user.Subscribe, servers []*server.Server, rules []*server.RuleGroup) ([]byte, string, error) {
proxyManager := adapter.NewAdapter(servers, rules)
clientType := l.getClientType(req)
var resp []byte
var err error
l.Logger.Info(fmt.Sprintf("[Generate Subscribe] %s", clientType), logger.Field("ua", req.UA), logger.Field("flag", req.Flag))
switch clientType {
case "clash":
resp, err = proxyManager.BuildClash(userSub.UUID)
if err != nil {
l.Errorw("[Generate Subscribe] build clash error", logger.Field("error", err.Error()))
return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build clash error: %v", err.Error())
}
l.setClashHeaders()
case "sing-box":
resp, err = proxyManager.BuildSingbox(userSub.UUID)
if err != nil {
l.Errorw("[Generate Subscribe] build sing-box error", logger.Field("error", err.Error()))
return nil, "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "build sing-box error: %v", err.Error())
}
case "quantumult":
resp = []byte(proxyManager.BuildQuantumultX(userSub.UUID))
case "shadowrocket":
resp = proxyManager.BuildShadowrocket(userSub.UUID, shadowrocket.UserInfo{
Upload: userSub.Upload,
Download: userSub.Download,
TotalTraffic: userSub.Traffic,
ExpiredDate: userSub.ExpireTime,
})
case "loon":
resp = proxyManager.BuildLoon(userSub.UUID)
case "surfboard":
subsURL := l.getSubscribeURL(userSub.Token)
resp = proxyManager.BuildSurfboard(l.svc.Config.Site.SiteName, surfboard.UserInfo{
Upload: userSub.Upload,
Download: userSub.Download,
TotalTraffic: userSub.Traffic,
ExpiredDate: userSub.ExpireTime,
UUID: userSub.UUID,
SubscribeURL: subsURL,
})
l.setSurfboardHeaders()
default:
resp = proxyManager.BuildGeneral(userSub.UUID)
}
headerInfo := fmt.Sprintf("upload=%d;download=%d;total=%d;expire=%d",
userSub.Upload, userSub.Download, userSub.Traffic, userSub.ExpireTime.Unix())
return resp, headerInfo, nil
}
func (l *SubscribeLogic) setClashHeaders() {
l.ctx.Header("content-disposition", fmt.Sprintf("tattachment;filename*=UTF-8''%s.yaml", l.svc.Config.Site.SiteName))
l.ctx.Header("Profile-Update-Interval", "24")
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
func (l *SubscribeLogic) setSurfboardHeaders() {
l.ctx.Header("content-disposition", fmt.Sprintf("attachment;filename*=UTF-8''%s.conf", l.svc.Config.Site.SiteName))
l.ctx.Header("Content-Type", "application/octet-stream; charset=UTF-8")
}
func (l *SubscribeLogic) getSubscribeURL(token string) string {
if l.svc.Config.Subscribe.PanDomain {
return fmt.Sprintf("https://%s", l.ctx.Request.Host)
}
if l.svc.Config.Subscribe.SubscribeDomain != "" {
domains := strings.Split(l.svc.Config.Subscribe.SubscribeDomain, "\n")
return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", domains[0], l.svc.Config.Subscribe.SubscribePath, token)
}
return fmt.Sprintf("https://%s%s?token=%s&flag=surfboard", l.ctx.Request.Host, l.svc.Config.Subscribe.SubscribePath, token)
}
func (l *SubscribeLogic) getClientType(req *types.SubscribeRequest) string {
clientTypeMap := map[string]string{
"clash": "clash",
"meta": "clash",
"sing-box": "sing-box",
"hiddify": "sing-box",
"surge": "surge",
"quantumult": "quantumult",
"shadowrocket": "shadowrocket",
"loon": "loon",
"surfboard": "surfboard",
}
findClient := func(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
if s == "" {
return ""
}
for key, clientType := range clientTypeMap {
if strings.Contains(s, key) {
return clientType
}
}
return ""
}
// 优先检查Flag参数
if typ := findClient(req.Flag); typ != "" {
return typ
}
// 其次检查UA参数
return findClient(req.UA)
}