Compare commits

...

11 Commits

Author SHA1 Message Date
505932d9d1 安全优化: 默认使用dev环境,main分支显式覆盖为生产环境
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 6m18s
2026-02-09 20:58:36 -08:00
bc665262ac 修改runner
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-02-09 20:54:14 -08:00
390ca150fe 修改runner
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-02-09 20:52:15 -08:00
f518b8b1eb x
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
2026-02-09 19:01:20 -08:00
28ada42ae5 208 2026-02-08 18:49:14 -08:00
709d657906 203 2026-02-03 04:40:23 -08:00
5b238919f5 x 2026-02-01 19:07:50 -08:00
af5231747a 201 2026-01-31 08:30:58 -08:00
16c261bd36 备份 2026-01-30 22:15:17 -08:00
d1d95618ad 邮件 2026-01-27 10:42:04 -08:00
48c92ea374 邀请 2026-01-27 03:13:15 -08:00
82 changed files with 5079 additions and 339 deletions

View File

@ -13,18 +13,18 @@ on:
env: env:
# Docker镜像仓库 # Docker镜像仓库
REPO: ${{ vars.REPO || 'registry.kxsw.us/ario-server' }} REPO: ${{ vars.REPO || 'registry.kxsw.us/vpn-server' }}
# SSH连接信息 # SSH连接信息 (默认为dev开发环境main分支会在动态环境变量步骤中覆盖为生产环境)
SSH_HOST: ${{ vars.SSH_HOST }} SSH_HOST: ${{ vars.DEV_SSH_HOST }}
SSH_PORT: ${{ vars.SSH_PORT }} SSH_PORT: ${{ vars.SSH_PORT }}
SSH_USER: ${{ vars.SSH_USER }} SSH_USER: ${{ vars.SSH_USER }}
SSH_PASSWORD: ${{ vars.SSH_PASSWORD }} SSH_PASSWORD: ${{ vars.DEV_SSH_PASSWORD }}
# TG通知 # TG通知
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0 TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
TG_CHAT_ID: "-4940243803" TG_CHAT_ID: "-4940243803"
# Go构建变量 # Go构建变量
SERVICE: ario SERVICE: vpn
SERVICE_STYLE: ario SERVICE_STYLE: vpn
VERSION: ${{ github.sha }} VERSION: ${{ github.sha }}
BUILDTIME: ${{ github.event.head_commit.timestamp }} BUILDTIME: ${{ github.event.head_commit.timestamp }}
GOARCH: amd64 GOARCH: amd64
@ -50,12 +50,14 @@ jobs:
echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV echo "DOCKER_TAG_SUFFIX=latest" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV echo "CONTAINER_NAME=ppanel-server" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
echo "为 main 分支设置生产环境变量" echo "SSH_HOST=${{ vars.SSH_HOST }}" >> $GITHUB_ENV
echo "SSH_PASSWORD=${{ vars.SSH_PASSWORD }}" >> $GITHUB_ENV
echo "为 main 分支设置生产环境变量 (部署到生产服务器)"
elif [ "${{ github.ref_name }}" = "dev" ]; then elif [ "${{ github.ref_name }}" = "dev" ]; then
echo "DOCKER_TAG_SUFFIX=dev" >> $GITHUB_ENV echo "DOCKER_TAG_SUFFIX=dev" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-dev" >> $GITHUB_ENV echo "CONTAINER_NAME=ppanel-server-dev" >> $GITHUB_ENV
echo "DEPLOY_PATH=/root/vpn_server_dev" >> $GITHUB_ENV echo "DEPLOY_PATH=/root/bindbox" >> $GITHUB_ENV
echo "为 dev 分支设置开发环境变量" echo "为 dev 分支设置开发环境变量 (部署到开发服务器)"
else else
echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV echo "DOCKER_TAG_SUFFIX=${{ github.ref_name }}" >> $GITHUB_ENV
echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV echo "CONTAINER_NAME=ppanel-server-${{ github.ref_name }}" >> $GITHUB_ENV

View File

@ -43,7 +43,7 @@ type (
GiftAmount int64 `json:"gift_amount"` GiftAmount int64 `json:"gift_amount"`
Telegram int64 `json:"telegram"` Telegram int64 `json:"telegram"`
ReferCode string `json:"refer_code"` ReferCode string `json:"refer_code"`
RefererId int64 `json:"referer_id"` RefererId *int64 `json:"referer_id"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
IsAdmin bool `json:"is_admin"` IsAdmin bool `json:"is_admin"`
MemberStatus string `json:"member_status"` MemberStatus string `json:"member_status"`
@ -295,3 +295,4 @@ service ppanel {
@handler GetUserLoginLogs @handler GetUserLoginLogs
get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse) get /login/logs (GetUserLoginLogsRequest) returns (GetUserLoginLogsResponse)
} }

View File

@ -118,6 +118,53 @@ type (
DeviceStatus bool `json:"device_status"` DeviceStatus bool `json:"device_status"`
EmailStatus bool `json:"email_status"` EmailStatus bool `json:"email_status"`
} }
// GetAgentRealtimeRequest - 获取代理链接实时数据
GetAgentRealtimeRequest {}
// GetAgentRealtimeResponse - 代理链接实时数据响应
GetAgentRealtimeResponse {
Total int64 `json:"total"` // 访问总人数
Clicks int64 `json:"clicks"` // 点击量
Views int64 `json:"views"` // 浏览量
PaidCount int64 `json:"paid_count"` // 付费数量
GrowthRate string `json:"growth_rate"` // 访问量环比增长率(例如:"+10.5%"、"-5.2%"、"0%"
PaidGrowthRate string `json:"paid_growth_rate"` // 付费用户环比增长率(例如:"+20.0%"、"-10.0%"、"0%"
}
// GetUserInviteStatsRequest - 获取用户邀请统计
GetUserInviteStatsRequest {}
// GetUserInviteStatsResponse - 用户邀请统计响应
GetUserInviteStatsResponse {
FriendlyCount int64 `json:"friendly_count"` // 有效邀请数(有订单的用户)
HistoryCount int64 `json:"history_count"` // 历史邀请总数
}
// GetInviteSalesRequest - 获取最近销售数据
GetInviteSalesRequest {
Page int `form:"page" validate:"required"`
Size int `form:"size" validate:"required"`
}
// GetInviteSalesResponse - 最近销售数据响应
GetInviteSalesResponse {
Total int64 `json:"total"` // 销售记录总数
List []InvitedUserSale `json:"list"` // 销售数据列表(分页)
}
// InvitedUserSale - 被邀请用户的销售记录
InvitedUserSale {
Amount float64 `json:"amount"`
CreatedAt int64 `json:"created_at"`
UserHash string `json:"user_hash"`
ProductName string `json:"product_name"`
}
// GetAgentDownloadsRequest - 获取各端下载量
GetAgentDownloadsRequest {}
// GetAgentDownloadsResponse - 各端下载量响应
GetAgentDownloadsResponse {
List []AgentDownloadStats `json:"list"`
}
// AgentDownloadStats - 各端下载量统计
AgentDownloadStats {
Platform string `json:"platform"`
Clicks int64 `json:"clicks"`
Visits int64 `json:"visits"`
}
) )
@server ( @server (
@ -225,5 +272,21 @@ service ppanel {
@doc "Unbind Device" @doc "Unbind Device"
@handler UnbindDevice @handler UnbindDevice
put /unbind_device (UnbindDeviceRequest) put /unbind_device (UnbindDeviceRequest)
@doc "Get agent realtime data"
@handler GetAgentRealtime
get /agent/realtime (GetAgentRealtimeRequest) returns (GetAgentRealtimeResponse)
@doc "Get user invite statistics"
@handler GetUserInviteStats
get /invite/stats (GetUserInviteStatsRequest) returns (GetUserInviteStatsResponse)
@doc "Get invite sales data"
@handler GetInviteSales
get /invite/sales (GetInviteSalesRequest) returns (GetInviteSalesResponse)
@doc "Get agent downloads data"
@handler GetAgentDownloads
get /agent/downloads (GetAgentDownloadsRequest) returns (GetAgentDownloadsResponse)
} }

21
batch_decrypt_logs.sh Normal file
View File

@ -0,0 +1,21 @@
#!/bin/bash
# 批量解密 Nginx 日志中的下载请求
# 用法: ./batch_decrypt_logs.sh [日志文件路径]
LOG_FILE="${1:-/var/log/nginx/access.log}"
if [ ! -f "$LOG_FILE" ]; then
echo "错误: 日志文件不存在: $LOG_FILE"
echo "用法: $0 [日志文件路径]"
exit 1
fi
echo "正在处理日志文件: $LOG_FILE"
echo "提取包含 /v1/common/client/download 的请求..."
echo ""
# 提取所有 download 请求并传递给解密工具
grep "/v1/common/client/download" "$LOG_FILE" | \
head -n 100 | \
xargs -I {} go run cmd/decrypt_download_data/main.go "{}"

View File

@ -0,0 +1,249 @@
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"strings"
pkgaes "github.com/perfect-panel/server/pkg/aes"
)
func main() {
// 通讯密钥
communicationKey := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx"
// 真实 Nginx 日志数据 - 从用户提供的日志中选取
sampleLogs := []string{
// 加密的下载请求 - 不同平台
`172.245.180.199 - - [02/Feb/2026:04:35:47 +0000] "GET /v1/common/client/download?data=JetaR6P9e8G5lZg2KRiAhV6c%2FdMilBtP78bKmsbAxL8%3D&time=2026-02-02T04:35:15.032000 HTTP/1.1" 200 201 "https://www.hifastvpn.com/" "AdsBot-Google (+http://www.google.com/adsbot.html)"`,
`172.245.180.199 - - [02/Feb/2026:04:35:47 +0000] "GET /v1/common/client/download?data=%2FFTAxtcEd%2F8T2MzKdxxrPfWBXk4pNPbQZB3p8Yrl8XQ%3D&time=2026-02-02T04:35:15.031000 HTTP/1.1" 200 181 "https://www.hifastvpn.com/" "AdsBot-Google (+http://www.google.com/adsbot.html)"`,
`172.245.180.199 - - [02/Feb/2026:04:35:47 +0000] "GET /v1/common/client/download?data=i18AVRwlVSuFrbf4NmId0RcTbj0tRJIBFHP0MxLjDmI%3D&time=2026-02-02T04:35:15.033000 HTTP/1.1" 200 201 "https://www.hifastvpn.com/" "AdsBot-Google (+http://www.google.com/adsbot.html)"`,
`172.245.180.199 - - [02/Feb/2026:04:50:50 +0000] "GET /v1/common/client/download?platform=mac HTTP/1.1" 200 113 "https://gethifast.net/" "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)"`,
`172.245.180.199 - - [02/Feb/2026:04:50:50 +0000] "GET /v1/common/client/download?platform=windows HTTP/1.1" 200 117 "https://gethifast.net/" "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)"`,
`172.245.180.199 - - [02/Feb/2026:05:24:16 +0000] "GET /v1/common/client/download?data=XfZsgEqUUQ0YBTT51ETQp2wheSvE4SRupBfYbiLnJOc%3D&time=2026-02-02T05:24:15.462000 HTTP/1.1" 200 181 "https://www.hifastvpn.com/" "Mozilla/5.0 (X11; CrOS x86_64 14541.0.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"`,
// 真实用户下载
`172.245.180.199 - - [02/Feb/2026:02:15:16 +0000] "GET /v1/common/client/download?data=XIZiz7c4sbUGE7Hl8fY6O2D5QKaZqx%2Fg81uR7kjenSg%3D&time=2026-02-02T02:15:16.337000 HTTP/1.1" 200 201 "https://hifastvpn.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"`,
`172.245.180.199 - - [02/Feb/2026:02:18:09 +0000] "GET /v1/common/client/download?data=aB0HistwZTIhxJh6yIds%2B6knoyZC17KyxaXvyd3Z5LY%3D&time=2026-02-02T02:18:06.301000 HTTP/1.1" 200 201 "https://hifastvpn.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36"`,
// 实际文件下载
`111.55.176.116 - - [02/Feb/2026:02:19:02 +0000] "GET /v1/common/client/download/file/android-1.0.0.apk HTTP/2.0" 200 18546688 "https://hifastvpn.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36"`,
`111.249.202.38 - - [02/Feb/2026:03:14:46 +0000] "GET /v1/common/client/download/file/mac-1.0.0.dmg HTTP/2.0" 200 72821392 "https://hifastvpn.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.7091.96 Safari/537.36"`,
// Windows 用户
`172.245.180.199 - - [02/Feb/2026:02:23:55 +0000] "GET /v1/common/client/download?data=t8OIVjnZx1N7w5ras4oVH9V0wz4JYlR7849WYKvbj9E%3D&time=2026-02-02T02:23:56.110000 HTTP/1.1" 200 201 "https://hifastvpn.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.7149.88 Safari/537.36"`,
// Mac 用户
`172.245.180.199 - - [02/Feb/2026:03:14:10 +0000] "GET /v1/common/client/download?data=mGKSxZtL7Ptf30MgFzBJPIsURC%2FkOf2lOGaXQOQ5Ft8%3D&time=2026-02-02T03:14:07.667000 HTTP/1.1" 200 181 "https://hifastvpn.com/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 12.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.7091.96 Safari/537.36"`,
// Android 移动端
`172.245.180.199 - - [02/Feb/2026:03:19:41 +0000] "GET /v1/common/client/download?data=y7gttvd%2BoKf9%2BZUeNTsOvuFHwOLFBByrNjkvhPkVykg%3D&time=2026-02-02T03:19:42.192000 HTTP/1.1" 200 201 "https://hifastvpn.com/" "Mozilla/5.0 (Linux; Android 15; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.7559.59 Mobile Safari/537.36"`,
`183.171.68.186 - - [02/Feb/2026:03:19:47 +0000] "GET /v1/common/client/download/file/android-1.0.0.apk HTTP/1.1" 200 179890 "https://hifastvpn.com/" "Mozilla/5.0 (Linux; Android 15; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.7559.59 Mobile Safari/537.36"`,
}
// 如果命令行提供了参数,使用命令行参数
if len(os.Args) > 1 {
sampleLogs = os.Args[1:]
}
fmt.Println("=== Nginx 下载日志解密工具 ===")
fmt.Printf("通讯密钥: %s\n\n", communicationKey)
// 统计数据
stats := make(map[string]int)
successCount := 0
for i, logLine := range sampleLogs {
// 提取日志条目
entry := extractLogEntry(logLine)
if entry.Data == "" && entry.Platform == "" {
fmt.Printf("--- 日志 #%d ---\n", i+1)
fmt.Println("⚠️ 跳过: 未找到 data 或 platform 参数\n")
continue
}
// 如果有 platform 参数(非加密),直接使用
if entry.Platform != "" {
fmt.Printf("--- 日志 #%d ---\n", i+1)
fmt.Printf("📍 IP地址: %s\n", entry.IP)
fmt.Printf("🌐 来源: %s\n", entry.Referer)
fmt.Printf("🔓 平台: %s (未加密)\n\n", entry.Platform)
stats[entry.Platform]++
successCount++
continue
}
// 处理加密的 data 参数
if entry.Data == "" {
continue
}
// URL 解码
decodedData, err := url.QueryUnescape(entry.Data)
if err != nil {
fmt.Printf("--- 日志 #%d ---\n", i+1)
fmt.Printf("❌ 错误: URL 解码失败: %v\n\n", err)
continue
}
// 提取 nonce (IV) - 从 time 参数转换
nonce := extractNonceFromTime(entry.Time)
// AES 解密
plainText, err := pkgaes.Decrypt(decodedData, communicationKey, nonce)
if err != nil {
fmt.Printf("--- 日志 #%d ---\n", i+1)
fmt.Printf("❌ 错误: 解密失败: %v\n", err)
fmt.Printf(" IP: %s, Nonce: %s\n\n", entry.IP, nonce)
continue
}
// 解析 JSON 获取平台信息
var result map[string]interface{}
if err := json.Unmarshal([]byte(plainText), &result); err == nil {
if platform, ok := result["platform"].(string); ok {
stats[platform]++
}
}
fmt.Printf("--- 日志 #%d ---\n", i+1)
fmt.Printf("📍 IP地址: %s\n", entry.IP)
fmt.Printf("🌐 来源: %s\n", entry.Referer)
fmt.Printf("🔓 解密内容: %s\n\n", plainText)
successCount++
}
// 输出统计信息
if successCount > 0 {
fmt.Println("=" + strings.Repeat("=", 50))
fmt.Printf("📊 统计信息 (成功解密: %d)\n", successCount)
fmt.Println("=" + strings.Repeat("=", 50))
for platform, count := range stats {
fmt.Printf(" %s: %d 次\n", platform, count)
}
fmt.Println()
}
}
// LogEntry 表示解析后的日志条目
type LogEntry struct {
IP string
Data string
Time string
Referer string
Platform string
}
// extractLogEntry 从日志行中提取所有关键信息
func extractLogEntry(logLine string) *LogEntry {
entry := &LogEntry{}
// 提取 IP 地址(第一个字段)
parts := strings.Fields(logLine)
if len(parts) > 0 {
entry.IP = parts[0]
}
// 提取 Referer 和 User-Agent
// Nginx combined 格式:... "请求" 状态码 字节数 "Referer" "User-Agent"
// 需要找到最后两对引号
quotes := []int{}
for i := 0; i < len(logLine); i++ {
if logLine[i] == '"' {
quotes = append(quotes, i)
}
}
// 至少需要 6 个引号: "GET ..." "Referer" "User-Agent"
if len(quotes) >= 6 {
// 倒数第 4 和第 3 个引号之间是 Referer
refererStart := quotes[len(quotes)-4]
refererEnd := quotes[len(quotes)-3]
entry.Referer = logLine[refererStart+1 : refererEnd]
// 倒数第 2 和第 1 个引号之间是 User-Agent
// 如果需要也可以提取
// uaStart := quotes[len(quotes)-2]
// uaEnd := quotes[len(quotes)-1]
// entry.UserAgent = logLine[uaStart+1 : uaEnd]
}
// 查找 ? 后面的查询字符串
idx := strings.Index(logLine, "?")
// 如果没有查询参数,检查是否是直接文件下载
if idx == -1 {
// 检查是否包含 /v1/common/client/download/file/
filePrefix := "/v1/common/client/download/file/"
fileIdx := strings.Index(logLine, filePrefix)
if fileIdx != -1 {
// 提取文件名部分
// URL 形式可能是: /v1/common/client/download/file/Hi%E5%BF%ABVPN-windows-1.0.0.exe HTTP/1.1
// 需要截取到空格
pathStart := fileIdx + len(filePrefix)
pathEnd := strings.Index(logLine[pathStart:], " ")
if pathEnd != -1 {
filePath := logLine[pathStart : pathStart+pathEnd]
// URL 解码
decodedPath, err := url.QueryUnescape(filePath)
if err == nil {
// 转换为小写以便匹配
lowerPath := strings.ToLower(decodedPath)
if strings.Contains(lowerPath, "windows") || strings.HasSuffix(lowerPath, ".exe") {
entry.Platform = "windows"
} else if strings.Contains(lowerPath, "mac") || strings.HasSuffix(lowerPath, ".dmg") {
entry.Platform = "mac"
} else if strings.Contains(lowerPath, "android") || strings.HasSuffix(lowerPath, ".apk") {
entry.Platform = "android"
} else if strings.Contains(lowerPath, "ios") || strings.HasSuffix(lowerPath, ".ipa") {
entry.Platform = "ios"
}
}
}
}
return entry
}
queryStr := logLine[idx+1:]
// 截取到空格或 HTTP/
endIdx := strings.Index(queryStr, " ")
if endIdx != -1 {
queryStr = queryStr[:endIdx]
}
// 解析查询参数
params := strings.Split(queryStr, "&")
for _, param := range params {
kv := strings.SplitN(param, "=", 2)
if len(kv) != 2 {
continue
}
switch kv[0] {
case "data":
entry.Data = kv[1]
case "time":
entry.Time = kv[1]
case "platform":
entry.Platform = kv[1]
}
}
return entry
}
// extractNonceFromTime 从 time 参数中提取 nonce
// time 格式: 2026-02-02T04:35:15.032000
// 需要转换为纳秒时间戳的十六进制
func extractNonceFromTime(timeStr string) string {
if timeStr == "" {
return ""
}
// URL 解码
decoded, err := url.QueryUnescape(timeStr)
if err != nil {
return ""
}
// 简化处理:直接使用整个时间字符串作为 nonce
// 因为原始代码使用 time.Now().UnixNano() 的十六进制
// 但是从日志中我们无法准确还原原始的 nonce
// 所以尝试使用 time 字符串本身
return decoded
}

View File

@ -96,6 +96,8 @@ func getServers() *service.Group {
// init service context // init service context
ctx := svc.NewServiceContext(c) ctx := svc.NewServiceContext(c)
// init system config
initialize.StartInitSystemConfig(ctx)
services := service.NewServiceGroup() services := service.NewServiceGroup()
services.Add(internal.NewService(ctx)) services.Add(internal.NewService(ctx))
services.Add(queue.NewService(ctx)) services.Add(queue.NewService(ctx))

View File

@ -0,0 +1,198 @@
package main
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/orm"
"github.com/perfect-panel/server/pkg/tool"
orderLogic "github.com/perfect-panel/server/queue/logic/order"
"github.com/redis/go-redis/v9"
)
func main() {
// 1. Setup Configuration
c := config.Config{
MySQL: orm.Config{
Addr: "127.0.0.1:3306",
Dbname: "dev_ppanel", // Using dev_ppanel as default, change if needed
Username: "root",
Password: "rootpassword",
Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai",
MaxIdleConns: 10,
MaxOpenConns: 10,
},
Redis: config.RedisConfig{
Host: "127.0.0.1:6379",
DB: 0,
},
Invite: config.InviteConfig{
GiftDays: 3, // Default gift days
},
}
// 2. Connect to Database & Redis
db, err := orm.ConnectMysql(orm.Mysql{Config: c.MySQL})
if err != nil {
panic(fmt.Sprintf("DB Connection failed: %v", err))
}
rds := redis.NewClient(&redis.Options{
Addr: c.Redis.Host,
DB: c.Redis.DB,
})
// 3. Initialize ServiceContext
serviceCtx := svc.NewServiceContext(c)
serviceCtx.DB = db
serviceCtx.Redis = rds
// We don't need queue/scheduler for this unit test
ctx := context.Background()
// 4. Run Scenarios
fmt.Println("=== Starting Invite Reward Test ===")
// Scenario 1: Commission 0 (Expect Gift Days)
runScenario(ctx, serviceCtx, "Scenario_0_Commission", 0)
// Scenario 2: Commission 10 (Expect Money)
runScenario(ctx, serviceCtx, "Scenario_10_Commission", 10)
}
func runScenario(ctx context.Context, s *svc.ServiceContext, name string, referralPercentage int64) {
fmt.Printf("\n--- Running %s (ReferralPercentage: %d%%) ---\n", name, referralPercentage)
// Update Config
s.Config.Invite.ReferralPercentage = referralPercentage
// Cleanup old data (Partial cleanup since we don't have email to query)
// We'll rely on unique ReferCode / UUIDs to avoid collisions but DB might grow.
// Actually we should try to clean up.
// Since we removed Email from struct, we can't use it to query easily unless we check `auth_methods`.
// For this test, let's just create new users.
// Create Referrer
referrer := &user.User{
Password: tool.EncodePassWord("123456"),
ReferCode: fmt.Sprintf("REF%d", time.Now().UnixNano())[:20],
ReferralPercentage: 0, // Use global settings
Commission: 0,
}
// Use DB directly to ensure ID is updated in struct
if err := s.DB.Create(referrer).Error; err != nil {
fmt.Printf("Create Referrer Failed: %v\n", err)
return
}
// Force active subscription for referrer so they can receive gift time
createActiveSubscription(ctx, s, referrer.Id)
fmt.Printf("Created Referrer: ID=%d, Commission=%d\n", referrer.Id, referrer.Commission)
// Create User (Invitee)
invitee := &user.User{
Password: tool.EncodePassWord("123456"),
RefererId: referrer.Id,
}
if err := s.DB.Create(invitee).Error; err != nil {
fmt.Printf("Create Invitee Failed: %v\n", err)
return
}
// Force active subscription for invitee to receive gift time
_ = createActiveSubscription(ctx, s, invitee.Id)
fmt.Printf("Created Invitee: ID=%d, RefererID=%d\n", invitee.Id, invitee.RefererId)
// Create Order
orderInfo := &order.Order{
OrderNo: tool.GenerateTradeNo(),
UserId: invitee.Id,
Amount: 10000, // 100.00
Price: 10000,
FeeAmount: 0,
Status: 2, // Paid
Type: 1, // Subscribe
IsNew: true,
SubscribeId: 1, // Assume plan 1 exists
Quantity: 1,
}
// We need a dummy subscribe plan in DB or use existing
ensureSubscribePlan(ctx, s, 1)
// Execute Logic
logic := orderLogic.NewActivateOrderLogic(s)
// We only simulate the commission part logic or NewPurchase
// logic.NewPurchase does a lot of things.
// Let's call NewPurchase to be realistic, but we need to ensure dependencies exist.
// Instead of full NewPurchase which might fail on other things,
// let's verify if we can just call handleCommission? No it's private.
// So we call NewPurchase.
err := logic.NewPurchase(ctx, orderInfo)
if err != nil {
fmt.Printf("NewPurchase failed (expected for mocked env): %v\n", err)
// If it failed because of things we don't care (like sending email), check data anyway
} else {
fmt.Println("NewPurchase executed successfully.")
}
// Wait for async goroutines
time.Sleep(2 * time.Second)
// Check Results
// 1. Check Referrer Commission
refRes, _ := s.UserModel.FindOne(ctx, referrer.Id)
fmt.Printf("Result Referrer Commission: %d (Expected: %d)\n", refRes.Commission, int64(float64(orderInfo.Amount)*float64(referralPercentage)/100))
// 2. Check Gift Days (Check expiration time changes)
// We compare with the initial subscription time
// But since we just created it, it's simpler to check if 'ExpiryTime' is far in the future or extended.
// For 0 commission, we expect gift days.
refSub, _ := s.UserModel.FindActiveSubscribe(ctx, referrer.Id)
invSub, _ := s.UserModel.FindActiveSubscribe(ctx, invitee.Id)
// Avoid panic if sub not found
if refSub != nil {
fmt.Printf("Result Referrer Sub Expire: %v\n", refSub.ExpireTime)
} else {
fmt.Println("Result Referrer Sub Expire: nil")
}
if invSub != nil {
// NewPurchase renews/creates sub, so it should be valid + duration
fmt.Printf("Result Invitee Sub Expire: %v\n", invSub.ExpireTime)
} else {
fmt.Println("Result Invitee Sub Expire: nil")
}
}
func createActiveSubscription(ctx context.Context, s *svc.ServiceContext, userId int64) *user.Subscribe {
sub := &user.Subscribe{
UserId: userId,
Status: 1,
ExpireTime: time.Now().Add(30 * 24 * time.Hour), // 30 days initial
Token: uuid.New().String(),
UUID: uuid.New().String(),
}
s.UserModel.InsertSubscribe(ctx, sub)
return sub
}
func ensureSubscribePlan(ctx context.Context, s *svc.ServiceContext, id int64) {
_, err := s.SubscribeModel.FindOne(ctx, id)
if err != nil {
s.SubscribeModel.Insert(ctx, &subscribe.Subscribe{
Id: id,
Name: "Test Plan",
UnitTime: "Day", // Days
UnitPrice: 100,
Sell: &[]bool{true}[0],
})
}
}

View File

@ -0,0 +1,101 @@
# OpenInstall API 测试结果
## 测试总结
✅ **成功连接到 OpenInstall API**
- API 基础 URL: `https://data.openinstall.com`
- 测试的接口端点工作正常
- HTTP 状态码: 200
## 当前问题
❌ **ApiKey 配置错误**
API 返回错误: `code=3, error="apiKey错误"`
## 问题分析
当前配置中:
- `AppKey: alf57p` - 这是应用的标识符(AppKey),用于 SDK 集成
- 但数据接口需要的是单独的 `apiKey`,这两者不同
## 解决方案
### 步骤 1: 在 OpenInstall 后台配置数据接口
1. 登录 OpenInstall 后台: https://www.openinstall.com
2. 找到 **【数据接口】-【接口配置】** 菜单
3. **开启数据接口开关**
4. 获取 `apiKey` (这是专门用于数据接口的密钥,不同于 AppKey)
### 步骤 2: 更新配置文件
`ppanel-server/etc/ppanel.yaml` 中添加 `ApiKey`:
```yaml
OpenInstall:
Enable: true
AppKey: "alf57p" # SDK 集成使用
ApiKey: "your_api_key_from_backend" # 数据接口使用
```
### 步骤 3: 重新测试
获取到正确的 apiKey 后,运行测试程序:
```bash
cd cmd/test_openinstall
go run main.go
```
## 测试接口说明
测试程序当前测试了以下接口:
### 1. 新增安装数据 (Growth Data)
- 端点: `/data/event/growth`
- 功能: 获取指定时间范围内的访问量、点击量、安装量、注册量及留存数据
- 参数:
- `apiKey`: 数据接口密钥
- `startDate`: 开始日期 (格式: 2006-01-02)
- `endDate`: 结束日期
- `statType`: 统计类型 (daily=按天, hourly=按小时, total=合计)
返回数据包括:
- `visit`: 访问量
- `click`: 点击量
- `install`: 安装量
- `register`: 注册量
- `survive_d1`: 1日留存
- `survive_d7`: 7日留存
- `survive_d30`: 30日留存
### 2. 渠道列表 (Channel List)
- 端点: `/data/channel/list`
- 功能: 获取 H5 渠道列表
- 参数:
- `apiKey`: 数据接口密钥
- `pageNum`: 页码
- `pageSize`: 每页数量
## 更多可用接口
OpenInstall 数据接口还提供以下功能:
- 渠道分组管理 (创建、修改、删除)
- 渠道管理 (创建、修改、删除、查询)
- 子渠道管理
- 存量设备数据
- 活跃数据统计
- 效果点数据
- 设备分布统计
详细文档: https://www.openinstall.com/doc/data.html
## 下一步建议
1. **配置 ApiKey**: 按照上述步骤在 OpenInstall 后台获取并配置 apiKey
2. **更新配置**: 将 apiKey 添加到 `ppanel.yaml` 配置文件
3. **更新代码**: 修改 `pkg/openinstall/openinstall.go` 实现真实的 API 调用
4. **测试验证**: 重新运行测试程序验证数据获取

View File

@ -0,0 +1,254 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
// OpenInstall 数据接口基础 URL
apiBaseURL = "https://data.openinstall.com"
// 您的 ApiKey (数据接口密钥)
apiKey = "a7596bc007f31a98ca551e33a75d3bb5997b0b94027c6e988d3c0af1"
)
// 通用响应结构
type APIResponse struct {
Code int `json:"code"`
Error *string `json:"error"`
Body json.RawMessage `json:"body"`
}
// 新增安装数据
type GrowthData struct {
Date string `json:"date"`
Visit int64 `json:"visit"` // 点击量
Click int64 `json:"click"` // 访问量
Install int64 `json:"install"` // 安装量
Register int64 `json:"register"` // 注册量
SurviveD1 int64 `json:"survive_d1"` // 1日留存
SurviveD7 int64 `json:"survive_d7"` // 7日留存
SurviveD30 int64 `json:"survive_d30"` // 30日留存
}
// 渠道列表数据
type ChannelData struct {
ChannelCode string `json:"channelCode"`
ChannelName string `json:"channelName"`
LinkURL string `json:"linkUrl"`
CreateTime string `json:"createTime"`
GroupName string `json:"groupName"`
}
func main() {
fmt.Println("========================================")
fmt.Println("OpenInstall API 测试程序")
fmt.Println("========================================")
fmt.Printf("ApiKey: %s\n", apiKey)
fmt.Printf("API Base URL: %s\n", apiBaseURL)
fmt.Println()
ctx := context.Background()
// 测试1: 获取新增安装数据最近7天
fmt.Println("测试1: 获取新增安装数据最近7天")
fmt.Println("========================================")
testGrowthData(ctx, 7)
fmt.Println()
// 测试2: 获取新增安装数据最近30天
fmt.Println("测试2: 获取新增安装数据最近30天")
fmt.Println("========================================")
testGrowthData(ctx, 30)
fmt.Println()
// 测试3: 获取渠道列表
fmt.Println("测试3: 获取渠道列表")
fmt.Println("========================================")
testChannelList(ctx)
fmt.Println()
fmt.Println("========================================")
fmt.Println("测试完成!")
fmt.Println("========================================")
}
// 测试获取新增安装数据
func testGrowthData(ctx context.Context, days int) {
// 设置查询时间范围
endDate := time.Now()
startDate := endDate.AddDate(0, 0, -days)
// 构建 API URL
apiURL := fmt.Sprintf("%s/data/event/growth", apiBaseURL)
params := url.Values{}
params.Add("apiKey", apiKey)
params.Add("startDate", startDate.Format("2006-01-02"))
params.Add("endDate", endDate.Format("2006-01-02"))
params.Add("statType", "daily") // daily = 按天统计, hourly = 按小时统计, total = 合计
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
fmt.Printf("请求 URL: %s\n", fullURL)
body, statusCode, err := makeRequest(ctx, fullURL)
if err != nil {
fmt.Printf("❌ 请求失败: %v\n", err)
return
}
fmt.Printf("HTTP 状态码: %d\n", statusCode)
if statusCode == 200 {
// 解析响应
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
fmt.Printf("❌ JSON 解析失败: %v\n", err)
printRawResponse(body)
return
}
if apiResp.Code == 0 {
fmt.Println("✅ 成功获取数据!")
// 解析业务数据
var growthData []GrowthData
if err := json.Unmarshal(apiResp.Body, &growthData); err != nil {
fmt.Printf("⚠️ 业务数据解析失败: %v\n", err)
printRawResponse(body)
return
}
// 格式化输出数据
fmt.Printf("\n共获取 %d 天的数据:\n", len(growthData))
fmt.Println("----------------------------------------")
for _, data := range growthData {
fmt.Printf("日期: %s\n", data.Date)
fmt.Printf(" 访问量(visit): %d\n", data.Visit)
fmt.Printf(" 点击量(click): %d\n", data.Click)
fmt.Printf(" 安装量(install): %d\n", data.Install)
fmt.Printf(" 注册量(register): %d\n", data.Register)
fmt.Printf(" 1日留存: %d\n", data.SurviveD1)
fmt.Printf(" 7日留存: %d\n", data.SurviveD7)
fmt.Printf(" 30日留存: %d\n", data.SurviveD30)
fmt.Println("----------------------------------------")
}
} else {
errMsg := "未知错误"
if apiResp.Error != nil {
errMsg = *apiResp.Error
}
fmt.Printf("❌ API 返回错误 (code=%d): %s\n", apiResp.Code, errMsg)
printRawResponse(body)
}
} else {
fmt.Printf("❌ HTTP 请求失败\n")
printRawResponse(body)
}
}
// 测试获取渠道列表
func testChannelList(ctx context.Context) {
// 构建 API URL
apiURL := fmt.Sprintf("%s/data/channel/list", apiBaseURL)
params := url.Values{}
params.Add("apiKey", apiKey)
params.Add("pageNum", "0")
params.Add("pageSize", "20")
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
fmt.Printf("请求 URL: %s\n", fullURL)
body, statusCode, err := makeRequest(ctx, fullURL)
if err != nil {
fmt.Printf("❌ 请求失败: %v\n", err)
return
}
fmt.Printf("HTTP 状态码: %d\n", statusCode)
if statusCode == 200 {
// 解析响应
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
fmt.Printf("❌ JSON 解析失败: %v\n", err)
printRawResponse(body)
return
}
if apiResp.Code == 0 {
fmt.Println("✅ 成功获取渠道列表!")
// 直接打印原始数据
printJSONResponse(apiResp.Body)
} else {
errMsg := "未知错误"
if apiResp.Error != nil {
errMsg = *apiResp.Error
}
fmt.Printf("❌ API 返回错误 (code=%d): %s\n", apiResp.Code, errMsg)
printRawResponse(body)
}
} else {
fmt.Printf("❌ HTTP 请求失败\n")
printRawResponse(body)
}
}
// 发送 HTTP 请求
func makeRequest(ctx context.Context, url string) ([]byte, int, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, 0, fmt.Errorf("创建请求失败: %w", err)
}
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.StatusCode, fmt.Errorf("读取响应失败: %w", err)
}
return body, resp.StatusCode, nil
}
// 打印原始响应
func printRawResponse(body []byte) {
fmt.Println("\n原始响应内容:")
var prettyJSON map[string]interface{}
if err := json.Unmarshal(body, &prettyJSON); err == nil {
formatted, _ := json.MarshalIndent(prettyJSON, "", " ")
fmt.Println(string(formatted))
} else {
fmt.Println(string(body))
}
}
// 打印 JSON 响应
func printJSONResponse(data json.RawMessage) {
var prettyJSON interface{}
if err := json.Unmarshal(data, &prettyJSON); err == nil {
formatted, _ := json.MarshalIndent(prettyJSON, "", " ")
fmt.Println(string(formatted))
} else {
fmt.Println(string(data))
}
}

View File

@ -0,0 +1,158 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
apiBaseURL = "https://data.openinstall.com"
apiKey = "a7596bc007f31a98ca551e33a75d3bb5997b0b94027c6e988d3c0af1"
)
type APIResponse struct {
Code int `json:"code"`
Error *string `json:"error"`
Body json.RawMessage `json:"body"`
}
type DistributionData struct {
Key string `json:"key"`
Value int64 `json:"value"`
}
func main() {
fmt.Println("========================================")
fmt.Println("测试 OpenInstall 新增设备分布接口")
fmt.Println("========================================")
fmt.Println()
ctx := context.Background()
// 获取当月数据
now := time.Now()
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
fmt.Printf("当月数据: %s 到 %s\n", startOfMonth.Format("2006-01-02"), now.Format("2006-01-02"))
fmt.Println("========================================")
// 测试各平台的数据
platforms := []struct {
name string
platform string
}{
{"iOS", "ios"},
{"Android", "android"},
{"HarmonyOS", "harmony"},
}
for _, p := range platforms {
fmt.Printf("\n平台: %s\n", p.name)
fmt.Println("----------------------------------------")
// 获取总量
data, err := getDeviceDistribution(ctx, startOfMonth, now, p.platform, "total")
if err != nil {
fmt.Printf("❌ 失败: %v\n", err)
continue
}
fmt.Println("✅ 成功获取数据:")
for _, item := range data {
fmt.Printf(" %s: %d\n", item.Key, item.Value)
}
}
// 测试不同的 sumBy 参数
fmt.Println("\n========================================")
fmt.Println("测试不同的分组方式 (iOS平台):")
fmt.Println("========================================")
sumByOptions := []string{
"total", // 总量
"system_version", // 系统版本
"app_version", // app版本
"brand_model", // 机型
}
for _, sumBy := range sumByOptions {
fmt.Printf("\nsumBy=%s:\n", sumBy)
fmt.Println("----------------------------------------")
data, err := getDeviceDistribution(ctx, startOfMonth, now, "ios", sumBy)
if err != nil {
fmt.Printf("❌ 失败: %v\n", err)
continue
}
if len(data) == 0 {
fmt.Println("⚠️ 无数据")
continue
}
fmt.Println("✅ 数据:")
for _, item := range data {
fmt.Printf(" %s: %d\n", item.Key, item.Value)
}
}
fmt.Println("\n========================================")
fmt.Println("测试完成!")
fmt.Println("========================================")
}
func getDeviceDistribution(ctx context.Context, startDate, endDate time.Time, platform, sumBy string) ([]DistributionData, error) {
apiURL := fmt.Sprintf("%s/data/sum/growth", apiBaseURL)
params := url.Values{}
params.Add("apiKey", apiKey)
params.Add("beginDate", startDate.Format("2006-01-02")) // 注意:使用 beginDate 而不是 startDate
params.Add("endDate", endDate.Format("2006-01-02"))
params.Add("platform", platform) // 平台过滤: ios, android, harmony
params.Add("sumBy", sumBy) // 分组方式
params.Add("excludeDuplication", "0") // 不排重
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 0 {
errMsg := "unknown error"
if apiResp.Error != nil {
errMsg = *apiResp.Error
}
return nil, fmt.Errorf("API error (code=%d): %s", apiResp.Code, errMsg)
}
var distData []DistributionData
if err := json.Unmarshal(apiResp.Body, &distData); err != nil {
return nil, fmt.Errorf("failed to parse distribution data: %w", err)
}
return distData, nil
}

View File

@ -0,0 +1,68 @@
package main
import (
"context"
"fmt"
"time"
"github.com/perfect-panel/server/pkg/openinstall"
)
func main() {
fmt.Println("========================================")
fmt.Println("OpenInstall 包测试")
fmt.Println("========================================")
fmt.Println()
// 使用真实的 ApiKey
apiKey := "a7596bc007f31a98ca551e33a75d3bb5997b0b94027c6e988d3c0af1"
client := openinstall.NewClient(apiKey)
ctx := context.Background()
endDate := time.Now()
startDate := endDate.AddDate(0, 0, -7) // 最近7天
fmt.Printf("获取统计数据:%s 到 %s\n", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"))
fmt.Println("========================================")
// 测试 GetPlatformStats
stats, err := client.GetPlatformStats(ctx, startDate, endDate)
if err != nil {
fmt.Printf("❌ 获取失败: %v\n", err)
return
}
fmt.Println("✅ 成功获取平台统计数据!")
fmt.Println()
for _, stat := range stats {
fmt.Printf("平台: %s\n", stat.Platform)
fmt.Printf(" 访问量(Visits): %d\n", stat.Visits)
fmt.Printf(" 点击量(Clicks): %d\n", stat.Clicks)
fmt.Println()
}
// 测试 GetGrowthData
fmt.Println("========================================")
fmt.Println("测试每日增长数据:")
fmt.Println("========================================")
growthData, err := client.GetGrowthData(ctx, startDate, endDate, "daily")
if err != nil {
fmt.Printf("❌ 获取失败: %v\n", err)
return
}
fmt.Printf("✅ 成功获取 %d 天的数据!\n\n", len(growthData))
for _, data := range growthData {
if data.Visit > 0 || data.Click > 0 || data.Install > 0 {
fmt.Printf("日期: %s - 访问:%d, 点击:%d, 安装:%d, 注册:%d\n",
data.Date, data.Visit, data.Click, data.Install, data.Register)
}
}
fmt.Println()
fmt.Println("========================================")
fmt.Println("测试完成!")
fmt.Println("========================================")
}

View File

@ -0,0 +1,147 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
const (
apiBaseURL = "https://data.openinstall.com"
apiKey = "a7596bc007f31a98ca551e33a75d3bb5997b0b94027c6e988d3c0af1"
)
type APIResponse struct {
Code int `json:"code"`
Error *string `json:"error"`
Body json.RawMessage `json:"body"`
}
type DistributionData struct {
Key string `json:"key"`
Value int64 `json:"value"`
}
func main() {
fmt.Println("========================================")
fmt.Println("测试各端下载量统计1月份完整数据")
fmt.Println("========================================")
fmt.Println()
ctx := context.Background()
// 测试1月份数据
now := time.Now()
startOfLastMonth := time.Date(now.Year(), now.Month()-1, 1, 0, 0, 0, 0, now.Location())
endOfLastMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).AddDate(0, 0, -1)
fmt.Printf("测试时间段: %s 到 %s\n", startOfLastMonth.Format("2006-01-02"), endOfLastMonth.Format("2006-01-02"))
fmt.Println("========================================\n")
// 获取各平台数据
platforms := []struct {
name string
platform string
display string
}{
{"iOS", "ios", "iPhone/iPad"},
{"Android", "android", "Android"},
}
totalCount := int64(0)
platformCounts := make(map[string]int64)
for _, p := range platforms {
fmt.Printf("获取 %s 平台数据...\n", p.name)
data, err := getDeviceDistribution(ctx, startOfLastMonth, endOfLastMonth, p.platform, "total")
if err != nil {
fmt.Printf(" ❌ 失败: %v\n\n", err)
continue
}
count := int64(0)
for _, item := range data {
count += item.Value
}
platformCounts[p.display] = count
totalCount += count
fmt.Printf(" ✅ %s: %d\n\n", p.display, count)
}
// 输出汇总
fmt.Println("========================================")
fmt.Println("汇总结果(按界面格式):")
fmt.Println("========================================")
fmt.Printf("\n各端下载量: %d\n", totalCount)
fmt.Println("----------------------------------------")
fmt.Printf("📱 iPhone/iPad: %d\n", platformCounts["iPhone/iPad"])
fmt.Printf("🤖 Android: %d\n", platformCounts["Android"])
fmt.Printf("💻 Windows: %d (暂不支持)\n", int64(0))
fmt.Printf("🍎 Mac: %d (暂不支持)\n\n", int64(0))
// 说明
fmt.Println("========================================")
fmt.Println("注意事项:")
fmt.Println("========================================")
fmt.Println("1. OpenInstall 统计的是「安装激活量」,非纯下载量")
fmt.Println("2. Windows/Mac 数据需要通过其他方式获取")
fmt.Println("3. 如需当月数据,请在月中测试")
}
func getDeviceDistribution(ctx context.Context, startDate, endDate time.Time, platform, sumBy string) ([]DistributionData, error) {
apiURL := fmt.Sprintf("%s/data/sum/growth", apiBaseURL)
params := url.Values{}
params.Add("apiKey", apiKey)
params.Add("beginDate", startDate.Format("2006-01-02"))
params.Add("endDate", endDate.Format("2006-01-02"))
params.Add("platform", platform)
params.Add("sumBy", sumBy)
params.Add("excludeDuplication", "0")
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 0 {
errMsg := "unknown error"
if apiResp.Error != nil {
errMsg = *apiResp.Error
}
return nil, fmt.Errorf("API error (code=%d): %s", apiResp.Code, errMsg)
}
var distData []DistributionData
if err := json.Unmarshal(apiResp.Body, &distData); err != nil {
return nil, fmt.Errorf("failed to parse distribution data: %w", err)
}
return distData, nil
}

View File

@ -0,0 +1,66 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/perfect-panel/server/pkg/openinstall"
)
func main() {
fmt.Println("========================================")
fmt.Println("测试 GetPlatformDownloads 功能")
fmt.Println("========================================")
fmt.Println()
// 使用真实的 ApiKey
apiKey := "a7596bc007f31a98ca551e33a75d3bb5997b0b94027c6e988d3c0af1"
client := openinstall.NewClient(apiKey)
ctx := context.Background()
// 调用 GetPlatformDownloads 获取当月数据+ 环比
platformDownloads, err := client.GetPlatformDownloads(ctx, "")
if err != nil {
fmt.Printf("❌ 获取失败: %v\n", err)
return
}
fmt.Println("✅ 成功获取各端下载量统计!")
fmt.Println()
// 格式化输出
data, _ := json.MarshalIndent(platformDownloads, "", " ")
fmt.Println(string(data))
fmt.Println()
fmt.Println("========================================")
fmt.Println("界面数据展示:")
fmt.Println("========================================")
fmt.Printf("\n各端下载量: %d\n", platformDownloads.Total)
fmt.Println("----------------------------------------")
fmt.Printf("📱 iPhone/iPad: %d\n", platformDownloads.IOS)
fmt.Printf("🤖 Android: %d\n", platformDownloads.Android)
fmt.Printf("💻 Windows: %d\n", platformDownloads.Windows)
fmt.Printf("🍎 Mac: %d\n\n", platformDownloads.Mac)
if platformDownloads.Comparison != nil {
fmt.Println("相比前一个月:")
if platformDownloads.Comparison.Change >= 0 {
fmt.Printf(" 📈 增长 %d (%.2f%%)\n",
platformDownloads.Comparison.Change,
platformDownloads.Comparison.ChangePercent)
} else {
fmt.Printf(" 📉 下降 %d (%.2f%%)\n",
-platformDownloads.Comparison.Change,
-platformDownloads.Comparison.ChangePercent)
}
fmt.Printf(" 上月总量: %d\n", platformDownloads.Comparison.LastMonthTotal)
}
fmt.Println("\n========================================")
fmt.Println("测试完成!")
fmt.Println("========================================")
}

View File

@ -4,13 +4,13 @@ Debug: false
JwtAuth: JwtAuth:
AccessSecret: 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918 AccessSecret: 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
AccessExpire: 604800 AccessExpire: 604800
MaxSessionsPerUser: 3 MaxSessionsPerUser: 2
Logger: Logger:
ServiceName: PPanel ServiceName: PPanel
Mode: console Mode: console
Encoding: plain Encoding: plain
TimeFormat: '2025-01-01 00:00:00.000' TimeFormat: '2006-01-02 15:04:05.000'
Path: logs Path: logs
Level: debug Level: debug
MaxContentLength: 0 MaxContentLength: 0

17
decrypt_download.sh Normal file
View File

@ -0,0 +1,17 @@
#!/bin/bash
# 解密 Nginx 下载日志中的 data 参数
# 使用方法:
# ./decrypt_download.sh "data=xxx&time=xxx"
# 或者直接传入整条日志
if [ $# -eq 0 ]; then
echo "使用方法:"
echo " $0 'data=JetaR6P9e8G5lZg2KRiAhV6c%2FdMilBtP78bKmsbAxL8%3D&time=2026-02-02T04:35:15.032000'"
echo " 或"
echo " $0 '172.245.180.199 - - [02/Feb/2026:04:35:47 +0000] \"GET /v1/common/client/download?data=JetaR6P9e8G5lZg2KRiAhV6c%2FdMilBtP78bKmsbAxL8%3D&time=2026-02-02T04:35:15.032000 HTTP/1.1\"'"
exit 1
fi
cd "$(dirname "$0")/.."
go run cmd/decrypt_download_data/main.go "$@"

View File

@ -1,6 +1,6 @@
# PPanel 服务部署 (云端/无源码版) # PPanel 服务部署 (云端/无源码版)
# 使用方法: # 使用方法:
# 1. 确保已将 docker-compose.cloud.yml, configs/, loki/ 目录上传到服务器同一目录 # 1. 确保已将 docker-compose.cloud.yml, configs/, loki/, grafana/, prometheus/ 目录上传到服务器同一目录
# 2. 确保 configs/ 目录下有 ppanel.yaml 配置文件 # 2. 确保 configs/ 目录下有 ppanel.yaml 配置文件
# 3. 确保 logs/ 目录存在 (mkdir logs) # 3. 确保 logs/ 目录存在 (mkdir logs)
# 4. 运行: docker-compose -f docker-compose.cloud.yml up -d # 4. 运行: docker-compose -f docker-compose.cloud.yml up -d
@ -10,19 +10,25 @@ services:
# 1. 业务后端 (PPanel Server) # 1. 业务后端 (PPanel Server)
# ---------------------------------------------------- # ----------------------------------------------------
ppanel-server: ppanel-server:
image: registry.kxsw.us/ario-server:${PPANEL_SERVER_TAG:-latest} image: registry.kxsw.us/vpn-server:${PPANEL_SERVER_TAG:-latest}
container_name: ppanel-server container_name: ppanel-server
restart: always restart: always
ports:
- "8080:8080" # 暴露端口供宿主机 Nginx 反代
volumes: volumes:
# 挂载配置文件和日志
- ./configs:/app/etc - ./configs:/app/etc
- ./logs:/app/logs - ./logs:/app/logs
environment: environment:
- TZ=Asia/Shanghai - TZ=Asia/Shanghai
networks: # 链路追踪配置 (OTLP)
- ppanel_net - OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4317
- OTEL_SERVICE_NAME=ppanel-server
- OTEL_TRACES_EXPORTER=otlp
- OTEL_METRICS_EXPORTER=prometheus # 指标由 tempo 抓取,不使用 OTLP
network_mode: host
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
logging: logging:
driver: "json-file" driver: "json-file"
options: options:
@ -31,6 +37,35 @@ services:
depends_on: depends_on:
- mysql - mysql
- redis - redis
- tempo
# ----------------------------------------------------
# 14. Tempo (链路追踪存储 - 替代/增强 Jaeger)
# ----------------------------------------------------
tempo:
image: grafana/tempo:2.4.1
container_name: ppanel-tempo
user: root
restart: always
command:
- "-config.file=/etc/tempo.yaml"
- "-target=all"
volumes:
- ./tempo/tempo-config.yaml:/etc/tempo.yaml # - tempo_data:/var/tempo
- ./tempo_data:/var/tempo # 改为映射到当前目录,确保数据彻底干净
ports:
- "3200:3200"
- "4317:4317"
- "4318:4318"
- "9095:9095"
networks:
- ppanel_net
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# ---------------------------------------------------- # ----------------------------------------------------
# 2. MySQL Database # 2. MySQL Database
@ -42,13 +77,24 @@ services:
ports: ports:
- "3306:3306" # 临时开放外部访问,用完记得关闭! - "3306:3306" # 临时开放外部访问,用完记得关闭!
environment: environment:
MYSQL_ROOT_PASSWORD: "ppanel_password" # 请修改为强密码 MYSQL_ROOT_PASSWORD: "jpcV41ppanel" # 请修改为强密码
MYSQL_DATABASE: "ppanel_db" MYSQL_DATABASE: "ppanel"
TZ: Asia/Shanghai TZ: Asia/Shanghai
command: --default-authentication-plugin=mysql_native_password command:
- --default-authentication-plugin=mysql_native_password
- --innodb_buffer_pool_size=16G
- --innodb_buffer_pool_instances=16
- --innodb_log_file_size=2G
- --innodb_flush_log_at_trx_commit=2
- --innodb_io_capacity=5000
- --max_connections=5000
volumes: volumes:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql
- ./mysql/init:/docker-entrypoint-initdb.d # 初始化脚本 ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
networks: networks:
- ppanel_net - ppanel_net
logging: logging:
@ -64,8 +110,19 @@ services:
image: redis:7.0 image: redis:7.0
container_name: ppanel-redis container_name: ppanel-redis
restart: always restart: always
ports:
- "6379:6379"
command:
- redis-server
- --tcp-backlog 65535
- --maxmemory-policy allkeys-lru
volumes: volumes:
- redis_data:/data - redis_data:/data
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
networks: networks:
- ppanel_net - ppanel_net
logging: logging:
@ -86,6 +143,8 @@ services:
- ./loki/loki-config.yaml:/etc/loki/local-config.yaml - ./loki/loki-config.yaml:/etc/loki/local-config.yaml
- loki_data:/loki - loki_data:/loki
command: -config.file=/etc/loki/local-config.yaml command: -config.file=/etc/loki/local-config.yaml
ports:
- "3100:3100"
networks: networks:
- ppanel_net - ppanel_net
logging: logging:
@ -107,6 +166,8 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# 采集当前目录下的 logs 文件夹 # 采集当前目录下的 logs 文件夹
- ./logs:/var/log/ppanel-server:ro - ./logs:/var/log/ppanel-server:ro
# 采集 Nginx 访问日志(用于追踪邀请码来源)
- /var/log/nginx:/var/log/nginx:ro
command: -config.file=/etc/promtail/config.yaml command: -config.file=/etc/promtail/config.yaml
networks: networks:
- ppanel_net - ppanel_net
@ -126,16 +187,16 @@ services:
container_name: ppanel-grafana container_name: ppanel-grafana
restart: always restart: always
ports: ports:
- "3000:3000" - "3333:3000"
environment: environment:
- GF_SECURITY_ADMIN_PASSWORD=admin - GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false - GF_USERS_ALLOW_SIGN_UP=false
- GF_FEATURE_TOGGLES_ENABLE=appObservability #- GF_INSTALL_PLUGINS=redis-datasource
volumes: volumes:
- grafana_data:/var/lib/grafana - grafana_data:/var/lib/grafana
# 自动加载数据源和仪表盘配置
- ./grafana/provisioning:/etc/grafana/provisioning - ./grafana/provisioning:/etc/grafana/provisioning
# 挂载本地仪表盘 JSON 文件目录 extra_hosts:
- ./grafana/dashboards:/var/lib/grafana/dashboards - "host.docker.internal:host-gateway"
networks: networks:
- ppanel_net - ppanel_net
depends_on: depends_on:
@ -158,10 +219,14 @@ services:
volumes: volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus - prometheus_data:/prometheus
extra_hosts:
- "host.docker.internal:host-gateway"
command: command:
- '--config.file=/etc/prometheus/prometheus.yml' - '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus' - '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle' - '--web.enable-lifecycle'
- '--web.enable-remote-write-receiver'
networks: networks:
- ppanel_net - ppanel_net
logging: logging:
@ -198,7 +263,7 @@ services:
restart: always restart: always
# 使用 host.docker.internal 访问宿主机 # 使用 host.docker.internal 访问宿主机
command: command:
- -nginx.scrape-uri=http://host.docker.internal:80/nginx_status - -nginx.scrape-uri=http://host.docker.internal:8090/nginx_status
extra_hosts: extra_hosts:
- "host.docker.internal:host-gateway" - "host.docker.internal:host-gateway"
networks: networks:
@ -230,28 +295,6 @@ services:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
# ----------------------------------------------------
# 11. Jaeger (链路追踪)
# ----------------------------------------------------
jaeger:
image: jaegertracing/all-in-one:latest
container_name: ppanel-jaeger
restart: always
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
environment:
- LOG_LEVEL=debug
- COLLECTOR_OTLP_ENABLED=true
networks:
- ppanel_net
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# ---------------------------------------------------- # ----------------------------------------------------
# 12. Node Exporter (宿主机监控) # 12. Node Exporter (宿主机监控)
# ---------------------------------------------------- # ----------------------------------------------------
@ -302,6 +345,8 @@ volumes:
loki_data: loki_data:
grafana_data: grafana_data:
prometheus_data: prometheus_data:
tempo_data:
networks: networks:
ppanel_net: ppanel_net:

View File

@ -1,23 +0,0 @@
version: '3'
services:
jaeger:
image: jaegertracing/all-in-one:latest
container_name: jaeger
ports:
- "16686:16686"
- "4317:4317"
- "4318:4318"
environment:
# - SPAN_STORAGE_TYPE=elasticsearch
# - ES_SERVER_URLS=http://elasticsearch:9200
- LOG_LEVEL=debug
- COLLECTOR_OTLP_ENABLED=true
deploy:
resources:
limits:
cpus: '0.8'
memory: 500M
reservations:
cpus: '0.05'
memory: 200M

View File

@ -1,13 +0,0 @@
version: '3'
services:
ppanel:
container_name: ppanel-server
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
volumes:
- ./etc/ppanel.yaml:/app/etc/ppanel.yaml
restart: always

View File

@ -0,0 +1,54 @@
# ALIGNMENT: 检查设备登录 IP 逻辑
## 原始需求
用户询问:
> "检查一下 设备登录的时候 有检查IP 么"
> "我需要知道 有没有IP 目前数据库都没有IP"
## 项目现状分析 (Context Analysis)
1. **数据模型 (`user.Device`)**:
- 存在 `Ip` 字段 (`varchar(255)`).
- 定义在 `/internal/model/user/user.go`
2. **业务逻辑 (`DeviceLoginLogic.go`)**:
- 在代码逻辑中,确实尝试获取 `req.IP` 并保存到数据库。
- 创建新设备时:`deviceInfo.Ip = req.IP`
- 记录登录日志时:`LoginIP: req.IP`
3. **关键问题点 (Root Cause)**:
- 输入参数定义在 `internal/types/types.go`:
```go
type DeviceLoginRequest struct {
// ...
IP string `header:"X-Original-Forwarded-For"`
// ...
}
```
- **当前仅支持 `X-Original-Forwarded-For` 请求头**
- 如果请求经过 Nginx、Cloudflare 等代理,但没有专门配置传递这个特定的 Header或者使用的是标准的 `X-Forwarded-For` / `X-Real-IP`,后端获取到的 `req.IP` 将为空字符串。
- 这就是导致“数据库都没有IP”的直接原因。
## 疑问澄清 (Questions)
我们需要确认修复方案:
1. **是否只需支持标准 Header?**
- 建议改为优先获取 `X-Forwarded-For`,其次 `X-Real-IP`,最后才是 `X-Original-Forwarded-For` 或直连 IP。
- go-zero 框架通常可以通过配置或中间件处理 IP或者我们在 struct tag 中调整。但 struct tag `header` 只能由 go-zero 的 rest 绑定一个特定的 key。
2. **是否需要记录 IP 归属地?**
- 目前逻辑只记录 IP 字符串,不解析归属地。需求中没提,暂时不作为重点,但可以确认一下。
## 建议方案
修改 `DeviceLoginRequest` 的定义可能不够灵活Header key 是固定的)。
更好的方式是:
1. **移除 Struct Tag 绑定**(或者保留作为备选)。
2. **在 Logic 中显式获取 IP**
- 从 `l.ctx` (Context) 中获取 `http.Request` (如果 go-zero 支持)。
- 或者在 Middleware 中解析真实 IP 并放入 Context。
- 或者简单点,修改 Struct Tag 为最常用的 `X-Forwarded-For` (如果确定环境是这样配置的)。
**最快修复**:
`internal/types/types.go` 中的 `X-Original-Forwarded-For` 改为 `X-Forwarded-For` (或者根据实际网关配置修改)。
但通常建议使用工具函数解析多种 Header。
## 下一步 (Next Step)
请确认是否要我修改代码以支持标准的 IP 获取方式(如 `X-Forwarded-For`

View File

@ -0,0 +1,36 @@
# DESIGN: Device Login IP Fix
## 目标
修复设备登录时无法获取真实 IP (`req.IP` 为空) 的问题,导致数据库未存储 IP。
## 现状
- `internal/types/types.go` 定义了 `DeviceLoginRequest`,其中 `IP` 字段绑定的是 `X-Original-Forwarded-For`
- 实际环境中Nginx/Cloudflare等通常使用 `X-Forwarded-For`
## 方案选择
由于项目使用 `go-zero` 并且存在 `.api` 文件,**最佳实践**是修改 `.api` 文件并重新生成代码。
但考虑到我无法运行 `goctl` (或者环境可能不一致),如果不重新生成而直接改 `types.go`,虽然能即时生效,但下次生成会被覆盖。
**然而**,鉴于我之前的操作已经直接修改过 `types.go` (Invite Sales Time Filter),且项目看似允许直接修改(或用户负责生成),我将**优先修改 `.api` 文件** 以保持源头正确,同时**手动同步修改 `types.go`** 以确保立即生效。
## 变更范围
### 1. API 定义 (`apis/auth/auth.api`)
- 修改 `DeviceLoginRequest` struct。
- 将 `header: X-Original-Forwarded-For` 改为 `header: X-Forwarded-For` (这是最通用的标准)。
### 2. 生成文件 (`internal/types/types.go`)
- 手动同步修改 `DeviceLoginRequest` 中的 Tag。
- 变为: `IP string header:"X-Forwarded-For"`
### 3. (可选增强) 业务逻辑 (`internal/logic/auth/deviceLoginLogic.go`)
- 由于 go-zero 的绑定机制比较“死”,如果 Tag 没取到值就是空的。Logic 层拿到空字符串也没办法再去 Context 捞(除非 Context 里存了 request
- 暂时只做 Tag 修改,因为这是最根本原因。
## 验证
- 检查代码变更。
- (无法直接测试 IP 获取,依赖用户部署验证)。
## 任务拆分
1. 修改 `apis/auth/auth.api`
2. 修改 `internal/types/types.go`

View File

@ -0,0 +1,102 @@
# Nginx 下载日志解密工具
## 简介
此工具用于解密 Nginx 访问日志中 `/v1/common/client/download` 接口的加密 `data` 参数。
通讯密钥:`c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx`
## 解密结果示例
从 Nginx 日志解密后,可以获得下载请求的详细信息,例如:
```json
{"platform":"windows"}
{"platform":"mac"}
{"platform":"android"}
{"platform":"ios"}
```
还可能包含邀请码信息:
```json
{"platform":"windows","invite_code":"ABC123"}
```
## 使用方法
### 方法 1: 使用 Shell 脚本(推荐)
```bash
# 解密单条日志
./decrypt_download.sh '172.245.180.199 - - [02/Feb/2026:04:35:47 +0000] "GET /v1/common/client/download?data=JetaR6P9e8G5lZg2KRiAhV6c%2FdMilBtP78bKmsbAxL8%3D&time=2026-02-02T04:35:15.032000 HTTP/1.1"'
# 解密多条日志
./decrypt_download.sh \
'data=JetaR6P9e8G5lZg2KRiAhV6c%2FdMilBtP78bKmsbAxL8%3D&time=2026-02-02T04:35:15.032000' \
'data=%2FFTAxtcEd%2F8T2MzKdxxrPfWBXk4pNPbQZB3p8Yrl8XQ%3D&time=2026-02-02T04:35:15.031000'
```
### 方法 2: 直接运行 Go 程序
```bash
go run cmd/decrypt_download_data/main.go
```
默认会解密内置的示例日志。
### 方法 3: 从 Nginx 日志文件批量解密
```bash
# 提取所有 download 请求并解密
grep "/v1/common/client/download" /var/log/nginx/access.log | \
while read line; do
./decrypt_download.sh "$line"
done
```
## 从 Nginx 服务器上使用
如果您在 Nginx 服务器上root@localhost7701),可以这样操作:
1. **查找所有 download 请求**
```bash
grep "/v1/common/client/download" /var/log/nginx/access.log
```
2. **统计各平台下载量**
先解密所有日志,然后统计:
```bash
# 需要将此工具复制到服务器,或在本地解密后统计
```
3. **实时监控**
```bash
tail -f /var/log/nginx/access.log | grep "/v1/common/client/download"
```
## 技术细节
### 加密方式
- **算法**AES-CBC with PKCS7 padding
- **密钥长度**256 位(通过 SHA256 哈希生成)
- **IV 生成**:基于时间戳的 MD5 哈希
### 参数说明
- `data`: URL 编码的 Base64 加密数据
- `time`: 用于生成 IV 的时间戳字符串
### 解密流程
1. URL 解码 `data` 参数
2. Base64 解码得到密文
3. 使用通讯密钥和 `time` 生成解密密钥和 IV
4. 使用 AES-CBC 解密得到原始 JSON 数据
## 相关文件
- `cmd/decrypt_download_data/main.go` - 解密工具主程序
- `decrypt_download.sh` - Shell 脚本快捷方式
- `pkg/aes/aes.go` - AES 加密解密库
## 注意事项
⚠️ **安全提示**:通讯密钥应妥善保管,不要泄露给未授权人员。

5
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/klauspost/compress v1.17.11 github.com/klauspost/compress v1.17.11
github.com/nyaruka/phonenumbers v1.5.0 github.com/nyaruka/phonenumbers v1.5.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.17.2
github.com/smartwalle/alipay/v3 v3.2.23 github.com/smartwalle/alipay/v3 v3.2.23
github.com/spf13/cast v1.7.0 // indirect github.com/spf13/cast v1.7.0 // indirect
github.com/spf13/cobra v1.8.1 github.com/spf13/cobra v1.8.1
@ -61,6 +61,7 @@ require (
github.com/goccy/go-json v0.10.4 github.com/goccy/go-json v0.10.4
github.com/golang-migrate/migrate/v4 v4.18.2 github.com/golang-migrate/migrate/v4 v4.18.2
github.com/spaolacci/murmur3 v1.1.0 github.com/spaolacci/murmur3 v1.1.0
github.com/zeromicro/go-zero v1.9.4
google.golang.org/grpc v1.65.0 google.golang.org/grpc v1.65.0
google.golang.org/protobuf v1.36.5 google.golang.org/protobuf v1.36.5
gorm.io/driver/sqlite v1.4.4 gorm.io/driver/sqlite v1.4.4
@ -135,6 +136,7 @@ require (
go.opentelemetry.io/otel/metric v1.29.0 // indirect go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.13.0 // indirect golang.org/x/arch v0.13.0 // indirect
golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect golang.org/x/exp v0.0.0-20240525044651-4c93da0ed11d // indirect
@ -145,4 +147,5 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )

14
go.sum
View File

@ -291,10 +291,12 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
@ -359,6 +361,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeromicro/go-zero v1.9.4 h1:aRLFoISqAYijABtkbliQC5SsI5TbizJpQvoHc9xup8k=
github.com/zeromicro/go-zero v1.9.4/go.mod h1:a17JOTch25SWxBcUgJZYps60hygK3pIYdw7nGwlcS38=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
@ -385,6 +389,8 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
@ -538,6 +544,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@ -558,4 +566,6 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U= k8s.io/apimachinery v0.31.1 h1:mhcUBbj7KUjaVhyXILglcVjuS4nYXiwC+KKFBgIVy7U=
k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= k8s.io/apimachinery v0.31.1/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -29,6 +29,8 @@ type Config struct {
Subscribe SubscribeConfig `yaml:"Subscribe"` Subscribe SubscribeConfig `yaml:"Subscribe"`
Invite InviteConfig `yaml:"Invite"` Invite InviteConfig `yaml:"Invite"`
Kutt KuttConfig `yaml:"Kutt"` Kutt KuttConfig `yaml:"Kutt"`
OpenInstall OpenInstallConfig `yaml:"OpenInstall"`
Loki LokiConfig `yaml:"Loki"`
Telegram Telegram `yaml:"Telegram"` Telegram Telegram `yaml:"Telegram"`
Log Log `yaml:"Log"` Log Log `yaml:"Log"`
Trace trace.Config `yaml:"Trace"` Trace trace.Config `yaml:"Trace"`
@ -207,7 +209,7 @@ type InviteConfig struct {
ForcedInvite bool `yaml:"ForcedInvite" default:"false"` ForcedInvite bool `yaml:"ForcedInvite" default:"false"`
ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"` ReferralPercentage int64 `yaml:"ReferralPercentage" default:"0"`
OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"` OnlyFirstPurchase bool `yaml:"OnlyFirstPurchase" default:"false"`
GiftDays int64 `yaml:"GiftDays" default:"0"` GiftDays int64 `yaml:"GiftDays" default:"3"`
} }
// KuttConfig Kutt 短链接服务配置 // KuttConfig Kutt 短链接服务配置
@ -219,6 +221,19 @@ type KuttConfig struct {
Domain string `yaml:"Domain" default:""` // 短链接域名 (例如: getsapp.net) Domain string `yaml:"Domain" default:""` // 短链接域名 (例如: getsapp.net)
} }
// OpenInstallConfig OpenInstall 配置
type OpenInstallConfig struct {
Enable bool `yaml:"Enable" default:"false"` // 是否启用 OpenInstall
AppKey string `yaml:"AppKey" default:""` // OpenInstall AppKey (SDK使用)
ApiKey string `yaml:"ApiKey" default:""` // OpenInstall 数据接口 ApiKey
}
// LokiConfig Loki 日志查询配置
type LokiConfig struct {
Enable bool `yaml:"Enable" default:"false"` // 是否启用 Loki 查询
URL string `yaml:"URL" default:"http://localhost:3100"` // Loki 服务地址
}
type Telegram struct { type Telegram struct {
Enable bool `yaml:"Enable" default:"false"` Enable bool `yaml:"Enable" default:"false"`
BotID int64 `yaml:"BotID" default:""` BotID int64 `yaml:"BotID" default:""`

View File

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -31,6 +32,9 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
return return
} }
// 统一处理邮箱格式:转小写并去空格,与发送验证码逻辑保持一致
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
// 校验邮箱验证码 // 校验邮箱验证码
if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil { if err := verifyEmailCode(c.Request.Context(), serverCtx, req.Email, req.Code); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
@ -38,7 +42,7 @@ func DeleteAccountHandler(serverCtx *svc.ServiceContext) gin.HandlerFunc {
} }
l := user.NewDeleteAccountLogic(c.Request.Context(), serverCtx) l := user.NewDeleteAccountLogic(c.Request.Context(), serverCtx)
resp, err := l.DeleteAccount() resp, err := l.DeleteAccountAll()
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get agent downloads data
func GetAgentDownloadsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetAgentDownloadsRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewGetAgentDownloadsLogic(c.Request.Context(), svcCtx)
resp, err := l.GetAgentDownloads(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get agent realtime data
func GetAgentRealtimeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetAgentRealtimeRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewGetAgentRealtimeLogic(c.Request.Context(), svcCtx)
resp, err := l.GetAgentRealtime(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,30 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get invite sales data
func GetInviteSalesHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetInviteSalesRequest
if err := c.ShouldBind(&req); err != nil {
result.ParamErrorResult(c, err)
return
}
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewGetInviteSalesLogic(c.Request.Context(), svcCtx)
resp, err := l.GetInviteSales(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -0,0 +1,26 @@
package user
import (
"github.com/gin-gonic/gin"
"github.com/perfect-panel/server/internal/logic/public/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/result"
)
// Get user invite statistics
func GetUserInviteStatsHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
return func(c *gin.Context) {
var req types.GetUserInviteStatsRequest
_ = c.ShouldBind(&req)
validateErr := svcCtx.Validate(&req)
if validateErr != nil {
result.ParamErrorResult(c, validateErr)
return
}
l := user.NewGetUserInviteStatsLogic(c.Request.Context(), svcCtx)
resp, err := l.GetUserInviteStats(&req)
result.HttpResult(c, resp, err)
}
}

View File

@ -40,7 +40,6 @@ import (
) )
func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) { func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
router.Use(middleware.TraceMiddleware(serverCtx))
adminAdsGroupRouter := router.Group("/v1/admin/ads") adminAdsGroupRouter := router.Group("/v1/admin/ads")
adminAdsGroupRouter.Use(middleware.AuthMiddleware(serverCtx)) adminAdsGroupRouter.Use(middleware.AuthMiddleware(serverCtx))
@ -895,6 +894,18 @@ func RegisterHandlers(router *gin.Engine, serverCtx *svc.ServiceContext) {
// Verify Email // Verify Email
publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx)) publicUserGroupRouter.POST("/verify_email", publicUser.VerifyEmailHandler(serverCtx))
// Get agent realtime data
publicUserGroupRouter.GET("/agent/realtime", publicUser.GetAgentRealtimeHandler(serverCtx))
// Get agent downloads data
publicUserGroupRouter.GET("/agent/downloads", publicUser.GetAgentDownloadsHandler(serverCtx))
// Get user invite statistics
publicUserGroupRouter.GET("/invite/stats", publicUser.GetUserInviteStatsHandler(serverCtx))
// Get invite sales data
publicUserGroupRouter.GET("/invite/sales", publicUser.GetInviteSalesHandler(serverCtx))
} }
serverGroupRouter := router.Group("/v1/server") serverGroupRouter := router.Group("/v1/server")

View File

@ -63,7 +63,7 @@ func SubscribeHandler(svcCtx *svc.ServiceContext) func(c *gin.Context) {
l := subscribe.NewSubscribeLogic(c, svcCtx) l := subscribe.NewSubscribeLogic(c, svcCtx)
resp, err := l.Handler(&req) resp, err := l.Handler(&req)
if err != nil { if err != nil {
c.String(http.StatusInternalServerError, "Internal Server") c.String(http.StatusInternalServerError, err.Error())
return return
} }
c.Header("subscription-userinfo", resp.Header) c.Header("subscription-userinfo", resp.Header)

View File

@ -52,6 +52,10 @@ func (l *DeleteUserDeviceLogic) DeleteUserDevice(req *types.DeleteUserDeivceRequ
_ = l.svcCtx.Redis.Del(ctx, sessionIdCacheKey).Err() _ = l.svcCtx.Redis.Del(ctx, sessionIdCacheKey).Err()
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, device.UserId) sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, device.UserId)
_ = l.svcCtx.Redis.ZRem(ctx, sessionsKey, sessionId).Err() _ = l.svcCtx.Redis.ZRem(ctx, sessionsKey, sessionId).Err()
l.Infow("[SessionMonitor] 管理员删除设备触发 Session 清理",
logger.Field("user_id", device.UserId),
logger.Field("session_id", sessionId),
logger.Field("device_id", device.Id))
} }
// 使用事务同时删除设备记录和关联的认证方式 // 使用事务同时删除设备记录和关联的认证方式

View File

@ -2,6 +2,7 @@ package auth
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"time" "time"
@ -35,6 +36,14 @@ func NewDeviceLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Devic
} }
func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) { func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *types.LoginResponse, err error) {
// 打印请求参数
l.Infow("DeviceLogin 请求参数",
logger.Field("identifier", req.Identifier),
logger.Field("ip", req.IP),
logger.Field("user_agent", req.UserAgent),
logger.Field("cf_token", req.CfToken),
)
if !l.svcCtx.Config.Device.Enable { if !l.svcCtx.Config.Device.Enable {
return nil, xerr.NewErrMsg("Device login is disabled") return nil, xerr.NewErrMsg("Device login is disabled")
} }
@ -97,6 +106,18 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
} }
} }
// [AuthDebug] Log detailed User Info
if userInfo != nil {
l.Infow("[AuthDebug] User Info Loaded",
logger.Field("user_id", userInfo.Id),
logger.Field("enable", userInfo.Enable),
logger.Field("is_admin", userInfo.IsAdmin),
logger.Field("balance", userInfo.Balance),
logger.Field("member_status", userInfo.MemberStatus),
logger.Field("created_at", userInfo.CreatedAt),
)
}
if createdNewDevice { if createdNewDevice {
deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier) deviceInfo, err = l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier)
if err != nil { if err != nil {
@ -118,24 +139,55 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
) )
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
} }
// [AuthDebug] Log detailed Device Info
l.Infow("[AuthDebug] Device Info",
logger.Field("device_id", deviceInfo.Id),
logger.Field("identifier", deviceInfo.Identifier),
logger.Field("ip", deviceInfo.Ip),
logger.Field("enabled", deviceInfo.Enabled),
logger.Field("online", deviceInfo.Online),
logger.Field("user_agent", deviceInfo.UserAgent),
)
} }
// Check if device has an existing valid session - reuse it instead of creating new one // Check if device has an existing valid session - reuse it instead of creating new one
var sessionId string var sessionId string
var reuseSession bool var reuseSession bool
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier) deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, req.Identifier)
l.Infow("[SESSION_DEBUG] logic start: checking device cache",
logger.Field("identifier", req.Identifier),
logger.Field("device_cache_key", deviceCacheKey),
)
if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && oldSid != "" { if oldSid, getErr := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); getErr == nil && oldSid != "" {
l.Infow("[SESSION_DEBUG] device cache hit",
logger.Field("identifier", req.Identifier),
logger.Field("old_session_id", oldSid),
)
// Check if old session is still valid AND belongs to current user // Check if old session is still valid AND belongs to current user
oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid) oldSessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, oldSid)
if uidStr, existErr := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); existErr == nil && uidStr != "" { if uidStr, existErr := l.svcCtx.Redis.Get(l.ctx, oldSessionKey).Result(); existErr == nil && uidStr != "" {
l.Infow("[SESSION_DEBUG] session cache hit",
logger.Field("old_session_id", oldSid),
logger.Field("session_user_id", uidStr),
logger.Field("current_user_id", userInfo.Id),
)
// Verify session belongs to current user (防止设备转移后复用其他用户的session) // Verify session belongs to current user (防止设备转移后复用其他用户的session)
if uidStr == fmt.Sprintf("%d", userInfo.Id) { if uidStr == fmt.Sprintf("%d", userInfo.Id) {
sessionId = oldSid sessionId = oldSid
reuseSession = true reuseSession = true
// Check TTL
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
l.Infow("reusing existing session for device", l.Infow("reusing existing session for device",
logger.Field("user_id", userInfo.Id), logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier), logger.Field("identifier", req.Identifier),
logger.Field("session_id", sessionId), logger.Field("session_id", sessionId),
logger.Field("session_ttl", ttl.Seconds()),
) )
} else { } else {
l.Infow("device session belongs to different user, creating new session", l.Infow("device session belongs to different user, creating new session",
@ -144,7 +196,17 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
logger.Field("identifier", req.Identifier), logger.Field("identifier", req.Identifier),
) )
} }
} else {
l.Infow("[SESSION_DEBUG] session cache miss or invalid",
logger.Field("old_session_id", oldSid),
logger.Field("error", existErr),
)
} }
} else {
l.Infow("[SESSION_DEBUG] device cache miss",
logger.Field("identifier", req.Identifier),
logger.Field("error", getErr),
)
} }
if !reuseSession { if !reuseSession {
sessionId = uuidx.NewUUID().String() sessionId = uuidx.NewUUID().String()
@ -156,10 +218,21 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
} }
// Generate token (always generate new token, but may reuse sessionId) // Generate token (always generate new token, but may reuse sessionId)
nowTime := time.Now().Unix()
accessExpire := l.svcCtx.Config.JwtAuth.AccessExpire
l.Infow("[AuthDebug] Generating Token",
logger.Field("iat", nowTime),
logger.Field("expire_seconds", accessExpire),
logger.Field("calculated_exp", nowTime+accessExpire),
logger.Field("user_id", userInfo.Id),
logger.Field("session_id", sessionId),
)
token, err := jwt.NewJwtToken( token, err := jwt.NewJwtToken(
l.svcCtx.Config.JwtAuth.AccessSecret, l.svcCtx.Config.JwtAuth.AccessSecret,
time.Now().Unix(), nowTime,
l.svcCtx.Config.JwtAuth.AccessExpire, accessExpire,
jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("UserId", userInfo.Id),
jwt.WithOption("SessionId", sessionId), jwt.WithOption("SessionId", sessionId),
jwt.WithOption("LoginType", "device"), jwt.WithOption("LoginType", "device"),
@ -199,6 +272,37 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set device id error: %v", err.Error()) return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set device id error: %v", err.Error())
} }
// [Debug] Store session detail for troubleshooting
// This helps us see "who is online" when multiple devices are logged in.
sessionDetail := map[string]interface{}{
"session_id": sessionId,
"user_id": userInfo.Id,
"identifier": req.Identifier,
"ip": req.IP,
"user_agent": req.UserAgent,
"login_time": time.Now().Format("2006-01-02 15:04:05"),
"device_id": deviceInfo.Id,
}
if detailJson, err := json.Marshal(sessionDetail); err == nil {
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sessionId)
_ = l.svcCtx.Redis.Set(l.ctx, detailKey, string(detailJson), time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
}
// 登录成功 - 打印详细信息用于调试
l.Infow("========== 设备登录成功 ==========",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("device_id", deviceInfo.Id),
logger.Field("session_id", sessionId),
logger.Field("reuse_session", reuseSession),
logger.Field("login_type", "device"),
logger.Field("login_ip", req.IP),
logger.Field("user_agent", req.UserAgent),
logger.Field("session_limit", l.svcCtx.SessionLimit()),
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
logger.Field("devices_count", len(userInfo.UserDevices)),
)
loginStatus = true loginStatus = true
return &types.LoginResponse{ return &types.LoginResponse{
Token: token, Token: token,

View File

@ -39,6 +39,18 @@ func NewEmailLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *EmailL
} }
func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.LoginResponse, err error) { func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.LoginResponse, err error) {
// 打印请求参数
l.Infow("EmailLogin 请求参数",
logger.Field("email", req.Email),
logger.Field("code", req.Code),
logger.Field("identifier", req.Identifier),
logger.Field("invite", req.Invite),
logger.Field("ip", req.IP),
logger.Field("user_agent", req.UserAgent),
logger.Field("login_type", req.LoginType),
logger.Field("cf_token", req.CfToken),
)
loginStatus := false loginStatus := false
var userInfo *user.User var userInfo *user.User
var isNewUser bool var isNewUser bool
@ -234,10 +246,14 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
if uidStr == fmt.Sprintf("%d", userInfo.Id) { if uidStr == fmt.Sprintf("%d", userInfo.Id) {
sessionId = oldSid sessionId = oldSid
reuseSession = true reuseSession = true
// Check TTL
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
l.Infow("reusing existing session for device", l.Infow("reusing existing session for device",
logger.Field("user_id", userInfo.Id), logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier), logger.Field("identifier", req.Identifier),
logger.Field("session_id", sessionId), logger.Field("session_id", sessionId),
logger.Field("session_ttl", ttl.Seconds()),
) )
} else { } else {
l.Infow("device session belongs to different user, creating new session", l.Infow("device session belongs to different user, creating new session",
@ -285,6 +301,21 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err() _ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
} }
// 登录成功 - 打印详细信息用于调试
l.Infow("========== 邮箱登录成功 ==========",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("device_id", deviceId),
logger.Field("session_id", sessionId),
logger.Field("reuse_session", reuseSession),
logger.Field("login_type", req.LoginType),
logger.Field("login_ip", req.IP),
logger.Field("user_agent", req.UserAgent),
logger.Field("session_limit", l.svcCtx.SessionLimit()),
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
logger.Field("devices_count", len(userInfo.UserDevices)),
)
loginStatus = true loginStatus = true
return &types.LoginResponse{ return &types.LoginResponse{
Token: token, Token: token,

View File

@ -40,6 +40,18 @@ func NewTelephoneLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Te
} }
func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r *http.Request, ip string) (resp *types.LoginResponse, err error) { func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r *http.Request, ip string) (resp *types.LoginResponse, err error) {
// 打印请求参数 (隐藏密码)
l.Infow("TelephoneLogin 请求参数",
logger.Field("telephone_area_code", req.TelephoneAreaCode),
logger.Field("telephone", req.Telephone),
logger.Field("has_password", req.Password != ""),
logger.Field("has_code", req.TelephoneCode != ""),
logger.Field("identifier", req.Identifier),
logger.Field("ip", ip),
logger.Field("user_agent", r.UserAgent()),
logger.Field("login_type", req.LoginType),
)
phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone) phoneNumber, err := phone.FormatToE164(req.TelephoneAreaCode, req.Telephone)
if err != nil { if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number") return nil, errors.Wrapf(xerr.NewErrCode(xerr.TelephoneError), "Invalid phone number")
@ -158,10 +170,14 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
if uidStr == fmt.Sprintf("%d", userInfo.Id) { if uidStr == fmt.Sprintf("%d", userInfo.Id) {
sessionId = oldSid sessionId = oldSid
reuseSession = true reuseSession = true
// Check TTL
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
l.Infow("reusing existing session for device", l.Infow("reusing existing session for device",
logger.Field("user_id", userInfo.Id), logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier), logger.Field("identifier", req.Identifier),
logger.Field("session_id", sessionId), logger.Field("session_id", sessionId),
logger.Field("session_ttl", ttl.Seconds()),
) )
} else { } else {
l.Infow("device session belongs to different user, creating new session", l.Infow("device session belongs to different user, creating new session",
@ -209,6 +225,21 @@ func (l *TelephoneLoginLogic) TelephoneLogin(req *types.TelephoneLoginRequest, r
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err() _ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
} }
loginStatus = true loginStatus = true
// 登录成功 - 打印详细信息用于调试
l.Infow("========== 手机登录成功 ==========",
logger.Field("user_id", userInfo.Id),
logger.Field("telephone", phoneNumber),
logger.Field("identifier", req.Identifier),
logger.Field("session_id", sessionId),
logger.Field("reuse_session", reuseSession),
logger.Field("login_type", req.LoginType),
logger.Field("login_ip", ip),
logger.Field("user_agent", r.UserAgent()),
logger.Field("session_limit", l.svcCtx.SessionLimit()),
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
logger.Field("devices_count", len(userInfo.UserDevices)),
)
return &types.LoginResponse{ return &types.LoginResponse{
Token: token, Token: token,
Limit: l.svcCtx.SessionLimit(), Limit: l.svcCtx.SessionLimit(),

View File

@ -38,6 +38,17 @@ func NewUserLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserLog
} }
func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.LoginResponse, err error) { func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.LoginResponse, err error) {
// 打印请求参数 (隐藏密码)
l.Infow("UserLogin 请求参数",
logger.Field("email", req.Email),
logger.Field("password_len", len(req.Password)),
logger.Field("identifier", req.Identifier),
logger.Field("ip", req.IP),
logger.Field("user_agent", req.UserAgent),
logger.Field("login_type", req.LoginType),
logger.Field("cf_token", req.CfToken),
)
loginStatus := false loginStatus := false
var userInfo *user.User var userInfo *user.User
// Record login status // Record login status
@ -130,10 +141,14 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
if uidStr == fmt.Sprintf("%d", userInfo.Id) { if uidStr == fmt.Sprintf("%d", userInfo.Id) {
sessionId = oldSid sessionId = oldSid
reuseSession = true reuseSession = true
// Check TTL
ttl, _ := l.svcCtx.Redis.TTL(l.ctx, oldSessionKey).Result()
l.Infow("reusing existing session for device", l.Infow("reusing existing session for device",
logger.Field("user_id", userInfo.Id), logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier), logger.Field("identifier", req.Identifier),
logger.Field("session_id", sessionId), logger.Field("session_id", sessionId),
logger.Field("session_ttl", ttl.Seconds()),
) )
} else { } else {
l.Infow("device session belongs to different user, creating new session", l.Infow("device session belongs to different user, creating new session",
@ -182,6 +197,21 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log
_ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err() _ = l.svcCtx.Redis.Set(l.ctx, deviceCacheKey, sessionId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err()
} }
// 登录成功 - 打印详细信息用于调试
l.Infow("========== 用户登录成功 ==========",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("device_id", deviceId),
logger.Field("session_id", sessionId),
logger.Field("reuse_session", reuseSession),
logger.Field("login_type", req.LoginType),
logger.Field("login_ip", req.IP),
logger.Field("user_agent", req.UserAgent),
logger.Field("session_limit", l.svcCtx.SessionLimit()),
logger.Field("auth_methods_count", len(userInfo.AuthMethods)),
logger.Field("devices_count", len(userInfo.UserDevices)),
)
loginStatus = true loginStatus = true
return &types.LoginResponse{ return &types.LoginResponse{
Token: token, Token: token,

View File

@ -32,7 +32,7 @@ func (l *GetDownloadLinkLogic) GetDownloadLink(req *types.GetDownloadLinkRequest
host := l.svcCtx.Config.Site.Host host := l.svcCtx.Config.Site.Host
if host == "" { if host == "" {
// 保底域名 // 保底域名
host = "tapi.airoport.co" host = "api.airoport.co"
} }
// 2. 版本号 (后续可以从数据库或配置中读取) // 2. 版本号 (后续可以从数据库或配置中读取)
@ -53,8 +53,14 @@ func (l *GetDownloadLinkLogic) GetDownloadLink(req *types.GetDownloadLinkRequest
ext = ".bin" ext = ".bin"
} }
// 4. 构建文件名: 平台-版本号-ic_邀请码.扩展名 // 4. 构建文件名: Hi快VPN-平台-版本号[-ic_邀请码].扩展名
filename := fmt.Sprintf("%s-%s-ic_%s%s", req.Platform, version, req.InviteCode, ext) 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 会拦截此路径进行虚拟更名处理) // 5. 构建完整 URL (Nginx 会拦截此路径进行虚拟更名处理)
url := fmt.Sprintf("https://%s/v1/common/client/download/file/%s", host, filename) url := fmt.Sprintf("https://%s/v1/common/client/download/file/%s", host, filename)

View File

@ -0,0 +1,64 @@
package common
import (
"context"
"testing"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/stretchr/testify/assert"
)
func TestGetDownloadLinkLogic_GetDownloadLink(t *testing.T) {
svcCtx := &svc.ServiceContext{
Config: config.Config{
Site: config.SiteConfig{
Host: "test.example.com",
},
},
}
ctx := context.Background()
l := NewGetDownloadLinkLogic(ctx, svcCtx)
tests := []struct {
name string
req *types.GetDownloadLinkRequest
wantSubStr []string // strings that should be in the URL
notSubStr []string // strings that should NOT be in the URL
}{
{
name: "With Invite Code",
req: &types.GetDownloadLinkRequest{
Platform: "windows",
InviteCode: "TESTCODE",
},
wantSubStr: []string{"-ic_TESTCODE.exe"},
notSubStr: []string{},
},
{
name: "Without Invite Code",
req: &types.GetDownloadLinkRequest{
Platform: "mac",
InviteCode: "",
},
wantSubStr: []string{".dmg"},
notSubStr: []string{"-ic", "ic_"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp, err := l.GetDownloadLink(tt.req)
assert.NoError(t, err)
assert.NotNil(t, resp)
for _, s := range tt.wantSubStr {
assert.Contains(t, resp.Url, s)
}
for _, s := range tt.notSubStr {
assert.NotContains(t, resp.Url, s)
}
})
}
}

View File

@ -88,7 +88,7 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
code := random.Key(6, 0) code := random.Key(6, 0)
taskPayload.Type = queue.EmailTypeVerify taskPayload.Type = queue.EmailTypeVerify
taskPayload.Email = req.Email taskPayload.Email = req.Email
taskPayload.Subject = "Verification code" taskPayload.Subject = "登录验证"
expireTime := l.svcCtx.Config.VerifyCode.ExpireTime expireTime := l.svcCtx.Config.VerifyCode.ExpireTime
if expireTime == 0 { if expireTime == 0 {

View File

@ -37,6 +37,11 @@ func NewEPayNotifyLogic(ctx *gin.Context, svcCtx *svc.ServiceContext) *EPayNotif
} }
func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error { func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
l.Logger.Info("[EPayNotify] 收到支付回调",
logger.Field("orderNo", req.OutTradeNo),
logger.Field("tradeNo", req.TradeNo),
logger.Field("tradeStatus", req.TradeStatus),
logger.Field("money", req.Money))
// Find payment config // Find payment config
data, ok := l.ctx.Request.Context().Value(constant.CtxKeyPayment).(*payment.Payment) data, ok := l.ctx.Request.Context().Value(constant.CtxKeyPayment).(*payment.Payment)
@ -51,6 +56,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo) return errors.Wrapf(xerr.NewErrCode(xerr.OrderNotExist), "order not exist: %v", req.OutTradeNo)
} }
l.Logger.Info("[EPayNotify] 找到订单",
logger.Field("orderNo", orderInfo.OrderNo),
logger.Field("currentStatus", orderInfo.Status),
logger.Field("userId", orderInfo.UserId),
logger.Field("orderType", orderInfo.Type))
var config payment.EPayConfig var config payment.EPayConfig
if err := json.Unmarshal([]byte(data.Config), &config); err != nil { if err := json.Unmarshal([]byte(data.Config), &config); err != nil {
l.Logger.Errorw("[EPayNotify] Unmarshal config failed", logger.Field("error", err.Error())) l.Logger.Errorw("[EPayNotify] Unmarshal config failed", logger.Field("error", err.Error()))
@ -59,7 +70,7 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
// Verify sign // Verify sign
client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type) client := epay.NewClient(config.Pid, config.Url, config.Key, config.Type)
if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug { if !client.VerifySign(urlParamsToMap(l.ctx.Request.URL.RawQuery)) && !l.svcCtx.Config.Debug {
l.Logger.Error("[EPayNotify] Verify sign failed") l.Logger.Error("[EPayNotify] Verify sign failed", logger.Field("orderNo", req.OutTradeNo))
return nil return nil
} }
if req.TradeStatus != "TRADE_SUCCESS" { if req.TradeStatus != "TRADE_SUCCESS" {
@ -67,9 +78,11 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
return nil return nil
} }
if orderInfo.Status == 5 { if orderInfo.Status == 5 {
l.Logger.Info("[EPayNotify] 订单已完成,跳过处理", logger.Field("orderNo", req.OutTradeNo))
return nil return nil
} }
// Update order status // Update order status
l.Logger.Info("[EPayNotify] 更新订单状态为已支付(2)", logger.Field("orderNo", req.OutTradeNo))
err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OutTradeNo, 2) err = l.svcCtx.OrderModel.UpdateOrderStatus(l.ctx, req.OutTradeNo, 2)
if err != nil { if err != nil {
l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo)) l.Logger.Error("[EPayNotify] Update order status failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
@ -87,10 +100,12 @@ func (l *EPayNotifyLogic) EPayNotify(req *types.EPayNotifyRequest) error {
task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes) task := asynq.NewTask(queueType.ForthwithActivateOrder, bytes)
taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task) taskInfo, err := l.svcCtx.Queue.EnqueueContext(l.ctx, task)
if err != nil { if err != nil {
l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error())) l.Logger.Error("[EPayNotify] Enqueue task failed", logger.Field("error", err.Error()), logger.Field("orderNo", req.OutTradeNo))
return err return err
} }
l.Logger.Info("[EPayNotify] Enqueue task success", logger.Field("taskInfo", taskInfo)) l.Logger.Info("[EPayNotify] ✅ 回调处理成功,已入队激活任务",
logger.Field("orderNo", req.OutTradeNo),
logger.Field("taskId", taskInfo.ID))
return nil return nil
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
@ -42,6 +43,7 @@ func NewBindEmailWithVerificationLogic(ctx context.Context, svcCtx *svc.ServiceC
// - *types.BindEmailWithVerificationResponse: 包含绑定结果、消息、token、用户ID // - *types.BindEmailWithVerificationResponse: 包含绑定结果、消息、token、用户ID
// - error: 发生错误时返回具体错误 // - error: 发生错误时返回具体错误
func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.BindEmailWithVerificationRequest) (*types.BindEmailWithVerificationResponse, error) { func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.BindEmailWithVerificationRequest) (*types.BindEmailWithVerificationResponse, error) {
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
// 获取当前用户 // 获取当前用户
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok { if !ok {
@ -135,6 +137,38 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
l.Infow("邮箱已存在,将设备转移到现有邮箱用户", l.Infow("邮箱已存在,将设备转移到现有邮箱用户",
logger.Field("email", req.Email), logger.Field("email", req.Email),
logger.Field("email_user_id", emailUserId)) logger.Field("email_user_id", emailUserId))
// 补全邀请人逻辑:如果邮箱账号没有邀请人,但设备账号有,则继承设备账号的邀请人
emailUser, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
if err == nil {
updates := make(map[string]interface{})
// 1. 处理 RefererId (邀请人)
if emailUser.RefererId == 0 && u.RefererId != 0 {
updates["referer_id"] = u.RefererId
l.Infow("将设备账号邀请人转移给邮箱账号",
logger.Field("email_user_id", emailUserId),
logger.Field("referer_id", u.RefererId))
}
// 2. 处理 ReferCode (如果邮箱账号意外没有邀请码,沿用设备的或生成新的) - 这是一个兜底,通常创建用户时已有
if emailUser.ReferCode == "" {
if u.ReferCode != "" {
updates["refer_code"] = u.ReferCode
} else {
updates["refer_code"] = uuidx.UserInviteCode(emailUserId)
}
}
if len(updates) > 0 {
if err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
return tx.Model(&user.User{}).Where("id = ?", emailUserId).Updates(updates).Error
}); err != nil {
l.Errorw("更新邮箱用户信息失败", logger.Field("error", err.Error()))
// 不阻断主流程
}
}
} else {
l.Errorw("查询目标邮箱用户失败,跳过邀请人合并", logger.Field("error", err.Error()))
}
// 创建前 需要 吧 原本的 user_devices 表中过的数据删除掉 防止出现两个记录 // 创建前 需要 吧 原本的 user_devices 表中过的数据删除掉 防止出现两个记录
devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, u.Id) devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, u.Id)
if err != nil { if err != nil {
@ -199,6 +233,17 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
// return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "创建邮箱用户设备记录失败") // return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "创建邮箱用户设备记录失败")
// } // }
} }
// 清除邮箱用户的缓存,确保更新(如邀请人、设备列表等)可见
userToClear := &user.User{Id: emailUserId}
// 添加当前邮箱做为 AuthMethod 以便 BatchClearRelatedCache 能清除相关索引缓存
// 注意:虽然 email 映射未变,但清除是一个好习惯,且 BatchClearRelatedCache 依赖 AuthMethods 来清除 email 缓存 key
userToClear.AuthMethods = []user.AuthMethods{
{AuthType: "email", AuthIdentifier: req.Email},
}
if err := l.svcCtx.UserModel.BatchClearRelatedCache(l.ctx, userToClear); err != nil {
l.Errorw("清理邮箱用户缓存失败", logger.Field("error", err.Error()), logger.Field("user_id", emailUserId))
}
// 4. 生成新的JWT token // 4. 生成新的JWT token
token, err := l.generateTokenForUser(emailUserId, deviceIdentifier) token, err := l.generateTokenForUser(emailUserId, deviceIdentifier)
if err != nil { if err != nil {
@ -432,7 +477,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
l.Errorw("清理原SessionId缓存失败", logger.Field("error", err.Error()), logger.Field("session_id", currentSessionId)) l.Errorw("清理原SessionId缓存失败", logger.Field("error", err.Error()), logger.Field("session_id", currentSessionId))
// 不返回错误,继续执行 // 不返回错误,继续执行
} else { } else {
l.Infow("已清理原SessionId缓存", logger.Field("session_id", currentSessionId)) l.Infow("[SessionMonitor] 绑定邮箱成功后立即清理原 Session", logger.Field("session_id", currentSessionId))
} }
} }

View File

@ -38,7 +38,7 @@ func (l *BindInviteCodeLogic) BindInviteCode(req *types.BindInviteCodeRequest) e
// 检查用户是否已经绑定过邀请码 // 检查用户是否已经绑定过邀请码
if currentUser.RefererId != 0 { if currentUser.RefererId != 0 {
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "user already bound invite code") return errors.Wrapf(xerr.NewErrCode(xerr.UserBindInviteCodeExist), "用户已绑定邀请人")
} }
// 查找邀请人 // 查找邀请人

View File

@ -131,6 +131,120 @@ func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse,
return resp, nil return resp, nil
} }
// DeleteAccountAll 注销账号逻辑 (全部解绑默认创建账号)
func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountResponse, err error) {
// 获取当前用户
currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// 获取当前调用设备 ID
currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64)
resp = &types.DeleteAccountResponse{}
var newUserId int64
// 开始数据库事务
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
// 1. 预先查找该用户下的所有设备记录 (因为稍后要迁移)
var userDevices []user.Device
if err := tx.Where("user_id = ?", currentUser.Id).Find(&userDevices).Error; err != nil {
l.Errorw("查询用户设备列表失败", logger.Field("user_id", currentUser.Id), logger.Field("error", err.Error()))
return err
}
// 如果没有识别到调用设备 ID记录日志但继续执行 (全量注销不应受限)
if currentDeviceId == 0 {
l.Infow("未识别到当前设备 ID将执行全量注销并尝试迁移所有已知设备", logger.Field("user_id", currentUser.Id), logger.Field("found_devices", len(userDevices)))
}
l.Infow("执行账号全量注销-迁移设备并删除旧数据", logger.Field("user_id", currentUser.Id), logger.Field("device_count", len(userDevices)))
// 2. 循环为每个设备创建新用户并迁移记录 (保留设备ID)
for _, dev := range userDevices {
// A. 创建新匿名用户
newUser, err := l.createAnonymousUser(tx)
if err != nil {
l.Errorw("为设备分配新用户主体失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
return err
}
// B. 迁移设备记录 (Update user_id)
if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil {
l.Errorw("迁移设备记录失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
return errors.Wrap(err, "迁移设备记录失败")
}
// C. 迁移设备认证方式 (Update user_id)
if err := tx.Model(&user.AuthMethods{}).
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", dev.Identifier).
Update("user_id", newUser.Id).Error; err != nil {
l.Errorw("迁移设备认证失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
return errors.Wrap(err, "迁移设备认证失败")
}
// 如果是当前请求的设备,记录其新 UserID 返回给前端
if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) {
newUserId = newUser.Id
}
l.Infow("旧设备已迁移至新匿名账号",
logger.Field("old_user_id", currentUser.Id),
logger.Field("new_user_id", newUser.Id),
logger.Field("device_id", dev.Id),
logger.Field("identifier", dev.Identifier))
}
// 3. 删除旧账号的剩余数据
// 删除剩余的认证方式 (排除已迁移的device类型剩下的如email/mobile等)
// 注意刚才已经把由currentUser拥有的device类型auth都迁移走了所以这里直接删剩下的即可
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil {
return errors.Wrap(err, "删除剩余认证方式失败")
}
// 设备记录已经全部迁移,理论上 user_id = currentUser.Id 的 device 应该没了,但为了保险可以删一下(或者是0)
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}).Error; err != nil {
return errors.Wrap(err, "删除残留设备记录失败")
}
// 删除所有订阅
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{}).Error; err != nil {
return errors.Wrap(err, "删除订阅失败")
}
// 删除用户主体
if err := tx.Delete(&user.User{}, currentUser.Id).Error; err != nil {
return errors.Wrap(err, "删除用户失败")
}
return nil
})
if err != nil {
return nil, err
}
// 最终清理所有 Session (踢掉所有设备)
l.clearAllSessions(currentUser.Id)
resp.Success = true
resp.Message = "注销成功"
resp.UserId = newUserId
resp.Code = 200
return resp, nil
}
// 辅助方法:通过 ID 查找 Identifier (以防 currentDeviceId 只是 ID)
func (l *DeleteAccountLogic) getIdentifierByDeviceID(devices []user.Device, id int64) string {
for _, d := range devices {
if d.Id == id {
return d.Identifier
}
}
return ""
}
// clearCurrentSession 清理当前请求的会话 // clearCurrentSession 清理当前请求的会话
func (l *DeleteAccountLogic) clearCurrentSession(userId int64) { func (l *DeleteAccountLogic) clearCurrentSession(userId int64) {
if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" { if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" {
@ -139,9 +253,40 @@ func (l *DeleteAccountLogic) clearCurrentSession(userId int64) {
// 从用户会话集合中移除当前session // 从用户会话集合中移除当前session
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
_ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err() _ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err()
l.Infow("[SessionMonitor] 注销账号清除 Session",
logger.Field("user_id", userId),
logger.Field("session_id", sessionId))
} }
} }
// clearAllSessions 清理指定用户的所有会话
func (l *DeleteAccountLogic) clearAllSessions(userId int64) {
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
// 获取所有 session id
sessions, err := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result()
if err != nil {
l.Errorw("获取用户会话列表失败", logger.Field("user_id", userId), logger.Field("error", err.Error()))
return
}
// 删除每个 session 的详情 key
for _, sid := range sessions {
sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sid)
_ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err()
// 同时尝试删除 detail key (如果存在)
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)
_ = l.svcCtx.Redis.Del(l.ctx, detailKey).Err()
}
// 删除用户的 session 集合 key
_ = l.svcCtx.Redis.Del(l.ctx, sessionsKey).Err()
l.Infow("[SessionMonitor] 注销账号-清除所有Session", logger.Field("user_id", userId), logger.Field("count", len(sessions)))
}
// generateReferCode 生成推荐码 // generateReferCode 生成推荐码
func generateReferCode() string { func generateReferCode() string {
bytes := make([]byte, 4) bytes := make([]byte, 4)
@ -195,3 +340,25 @@ func (l *DeleteAccountLogic) registerUserAndDevice(tx *gorm.DB, identifier, ip,
return userInfo, nil return userInfo, nil
} }
// createAnonymousUser 创建一个新的匿名用户主体 (仅User表)
func (l *DeleteAccountLogic) createAnonymousUser(tx *gorm.DB) (*user.User, error) {
// 1. 创建新用户
userInfo := &user.User{
Salt: "default",
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
}
if err := tx.Create(userInfo).Error; err != nil {
l.Errorw("failed to create user", logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err)
}
// 2. 更新推荐码
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil {
l.Errorw("failed to update refer code", logger.Field("user_id", userInfo.Id), logger.Field("error", err.Error()))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err)
}
return userInfo, nil
}

View File

@ -0,0 +1,301 @@
package user
import (
"context"
"fmt"
"os"
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/constant"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func createTestSvcCtx(t *testing.T, testName string) (*svc.ServiceContext, *gorm.DB, *miniredis.Miniredis) {
// 1. Setup Miniredis
mr, err := miniredis.Run()
assert.NoError(t, err)
rdb := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
// 2. Setup GORM with SQLite (File based for reliability)
dbName := fmt.Sprintf("test_%s.db", testName)
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
assert.NoError(t, err)
t.Cleanup(func() {
os.Remove(dbName)
})
// Migrate tables (Using Migrator to bypass index collision errors on global schema in SQLite)
_ = db.Migrator().CreateTable(&user.User{})
_ = db.Migrator().CreateTable(&user.Device{})
_ = db.Migrator().CreateTable(&user.AuthMethods{})
_ = db.Migrator().CreateTable(&user.Subscribe{})
// 3. Create ServiceContext
c := config.Config{}
c.Invite.OnlyFirstPurchase = true
svcCtx := &svc.ServiceContext{
Redis: rdb,
DB: db,
Config: c,
UserModel: user.NewModel(db, rdb),
}
return svcCtx, db, mr
}
func TestDeleteAccount_Guest_SingleDevice(t *testing.T) {
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
defer mr.Close()
// Setup: User, 1 Device, No Email
u := &user.User{
Id: 1,
ReferCode: "ref1",
}
db.Create(u)
device := &user.Device{
Id: 10,
UserId: 1,
Identifier: "device1_id",
}
db.Create(device)
auth := &user.AuthMethods{
UserId: 1,
AuthType: "device",
AuthIdentifier: "device1_id",
}
db.Create(auth)
// Context
ctx := context.Background()
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, int64(10))
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session1")
// Run Logic
l := NewDeleteAccountLogic(ctx, svcCtx)
resp, err := l.DeleteAccountAll()
if err != nil {
t.Fatalf("DeleteAccountAll failed: %v", err)
}
assert.True(t, resp.Success)
// Assertions for Guest User (Should be DELETED)
// Because 1 auth (device) and 1 device count -> isMainAccount = false
// Check Old User deleted
var userCount int64
db.Model(&user.User{}).Where("refer_code = ?", "ref1").Count(&userCount)
assert.Equal(t, int64(0), userCount, "Old User (by refer code) should be deleted")
// Device record (ID 10) should PRESERVED but have a NEW user_id
var updatedDevice user.Device
err = db.Model(&user.Device{}).Where("id = ?", 10).First(&updatedDevice).Error
assert.NoError(t, err, "Device record should still exist")
assert.NotEqual(t, int64(1), updatedDevice.UserId, "Device should have a new user ID")
assert.Equal(t, "device1_id", updatedDevice.Identifier, "Device identifier should remain unchanged")
// Check AuthMethod updated
var updatedAuth user.AuthMethods
err = db.Where("auth_type = ? AND auth_identifier = ?", "device", "device1_id").First(&updatedAuth).Error
assert.NoError(t, err, "AuthMethod should still exist")
assert.Equal(t, updatedDevice.UserId, updatedAuth.UserId, "AuthMethod should link to new user ID")
}
func TestDeleteAccount_User_WithEmail(t *testing.T) {
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
defer mr.Close()
// Setup: User, 1 Device, 1 Email
u := &user.User{
Id: 2,
}
db.Create(u)
device := &user.Device{
Id: 20,
UserId: 2,
Identifier: "device2_id",
}
db.Create(device)
authDevice := &user.AuthMethods{
UserId: 2,
AuthType: "device",
AuthIdentifier: "device2_id",
}
db.Create(authDevice)
authEmail := &user.AuthMethods{
UserId: 2,
AuthType: "email",
AuthIdentifier: "test@example.com",
}
db.Create(authEmail)
// Context
ctx := context.Background()
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, int64(20))
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session2")
// Run Logic
l := NewDeleteAccountLogic(ctx, svcCtx)
resp, err := l.DeleteAccountAll()
if err != nil {
t.Fatalf("DeleteAccountAll failed: %v", err)
}
assert.True(t, resp.Success)
// Assertions for Email User (Should BE deleted now)
var userCount int64
db.Model(&user.User{}).Where("id = ?", 2).Count(&userCount)
assert.Equal(t, int64(0), userCount, "User should be deleted")
// Device record (ID 20) should PRESERVED with NEW user_id
var updatedDevice user.Device
err = db.Model(&user.Device{}).Where("id = ?", 20).First(&updatedDevice).Error
assert.NoError(t, err, "Device record should still exist")
assert.NotEqual(t, int64(2), updatedDevice.UserId, "Device should have a new user ID")
// Device Auth should PRESERVED with NEW user_id
var updatedAuthDevice user.AuthMethods
err = db.Model(&user.AuthMethods{}).Where("auth_type = 'device' AND auth_identifier = 'device2_id'").First(&updatedAuthDevice).Error
assert.NoError(t, err, "Device auth should still exist")
assert.Equal(t, updatedDevice.UserId, updatedAuthDevice.UserId)
// Email Auth should be REMOVED
var authEmailCount int64
db.Model(&user.AuthMethods{}).Where("user_id = ? AND auth_type = 'email'", 2).Count(&authEmailCount)
assert.Equal(t, int64(0), authEmailCount, "Email auth should be removed")
}
func TestDeleteAccount_User_MultiDevice(t *testing.T) {
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
defer mr.Close()
// Setup: User, 2 Devices
u := &user.User{
Id: 3,
}
db.Create(u)
// Device 1 (Current)
device1 := &user.Device{
Id: 31,
UserId: 3,
Identifier: "device3_1",
}
db.Create(device1)
auth1 := &user.AuthMethods{
UserId: 3,
AuthType: "device",
AuthIdentifier: "device3_1",
}
db.Create(auth1)
// Device 2 (Other)
device2 := &user.Device{
Id: 32,
UserId: 3,
Identifier: "device3_2",
}
db.Create(device2)
auth2 := &user.AuthMethods{
UserId: 3,
AuthType: "device",
AuthIdentifier: "device3_2",
}
db.Create(auth2)
// Context
ctx := context.Background()
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, int64(31)) // Current = Device 1
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session3")
// Run Logic
l := NewDeleteAccountLogic(ctx, svcCtx)
resp, err := l.DeleteAccountAll()
if err != nil {
t.Fatalf("DeleteAccountAll failed: %v", err)
}
assert.True(t, resp.Success)
// Assertions for Multi Device User (Should BE deleted now)
var userCount int64
db.Model(&user.User{}).Where("id = ?", 3).Count(&userCount)
assert.Equal(t, int64(0), userCount, "Old User should be deleted")
// Device 1 (ID 31) should be PRESERVED
var updatedDev1 user.Device
err = db.Model(&user.Device{}).Where("id = ?", 31).First(&updatedDev1).Error
assert.NoError(t, err, "Device 1 should still exist")
assert.NotEqual(t, int64(3), updatedDev1.UserId, "Device 1 should have new UserID")
// Device 2 (ID 32) should be PRESERVED
var updatedDev2 user.Device
err = db.Model(&user.Device{}).Where("id = ?", 32).First(&updatedDev2).Error
assert.NoError(t, err, "Device 2 should still exist")
assert.NotEqual(t, int64(3), updatedDev2.UserId, "Device 2 should have new UserID")
// Verify they are independent users
assert.NotEqual(t, updatedDev1.UserId, updatedDev2.UserId, "Devices should have independent user accounts")
}
func TestDeleteAccount_MissingDeviceID(t *testing.T) {
svcCtx, db, mr := createTestSvcCtx(t, t.Name())
defer mr.Close()
// Setup: User, 1 Device, but context missing DeviceID
u := &user.User{
Id: 4,
ReferCode: "ref4",
}
db.Create(u)
device := &user.Device{
Id: 40,
UserId: 4,
Identifier: "device4_id",
}
db.Create(device)
// Context (Missing DeviceID)
ctx := context.Background()
ctx = context.WithValue(ctx, constant.CtxKeyUser, u)
ctx = context.WithValue(ctx, constant.CtxKeySessionID, "session4")
// Run Logic
l := NewDeleteAccountLogic(ctx, svcCtx)
resp, err := l.DeleteAccountAll()
if err != nil {
t.Fatalf("DeleteAccountAll failed: %v", err)
}
assert.True(t, resp.Success)
// Assertions: User should be deleted
var userCount int64
db.Model(&user.User{}).Where("refer_code = ?", "ref4").Count(&userCount)
assert.Equal(t, int64(0), userCount, "User should be deleted even without device context")
// Device record (ID 40) should be PRESERVED
var updatedDevice user.Device
err = db.Model(&user.Device{}).Where("id = ?", 40).First(&updatedDevice).Error
assert.NoError(t, err, "Device record should still exist")
assert.NotEqual(t, int64(4), updatedDevice.UserId, "Device should have a new user ID")
}

View File

@ -0,0 +1,94 @@
package user
import (
"context"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetAgentDownloadsLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// NewGetAgentDownloadsLogic 创建 GetAgentDownloadsLogic 实例
func NewGetAgentDownloadsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentDownloadsLogic {
return &GetAgentDownloadsLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// PlatformStats 各平台设备统计结果
type PlatformStats struct {
Android int64 `gorm:"column:android"`
IOS int64 `gorm:"column:ios"`
Mac int64 `gorm:"column:mac"`
Windows int64 `gorm:"column:windows"`
Total int64 `gorm:"column:total"`
}
// GetAgentDownloads 获取用户代理下载统计数据
// 基于用户邀请码查询被邀请用户的设备UA来统计各平台安装量
func (l *GetAgentDownloadsLogic) GetAgentDownloads(req *types.GetAgentDownloadsRequest) (resp *types.GetAgentDownloadsResponse, err error) {
// 1. 从 context 获取用户信息
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
l.Errorw("[GetAgentDownloads] user not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
// 2. 通过数据库查询各平台设备安装量
// 基于 user_device 表的 user_agent 字段判断平台
// UA格式: HiVPN/1.0.0 (平台; 设备; 版本) Flutter
var stats PlatformStats
err = l.svcCtx.DB.WithContext(l.ctx).
Table("user u").
Select(`
SUM(CASE WHEN d.user_agent LIKE '%(Android;%' THEN 1 ELSE 0 END) AS android,
SUM(CASE WHEN d.user_agent LIKE '%(iOS;%' THEN 1 ELSE 0 END) AS ios,
SUM(CASE WHEN d.user_agent LIKE '%(macOS;%' THEN 1 ELSE 0 END) AS mac,
SUM(CASE WHEN d.user_agent LIKE '%(Windows;%' THEN 1 ELSE 0 END) AS windows,
COUNT(*) AS total
`).
Joins("JOIN user_device d ON u.id = d.user_id").
Where("u.referer_id = ?", u.Id).
Scan(&stats).Error
if err != nil {
l.Errorw("[GetAgentDownloads] query platform stats failed",
logger.Field("error", err.Error()),
logger.Field("user_id", u.Id))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
"query platform stats failed: %v", err.Error())
}
l.Infow("[GetAgentDownloads] platform stats fetched successfully",
logger.Field("user_id", u.Id),
logger.Field("refer_code", u.ReferCode),
logger.Field("android", stats.Android),
logger.Field("ios", stats.IOS),
logger.Field("mac", stats.Mac),
logger.Field("windows", stats.Windows),
logger.Field("total", stats.Total))
// 3. 构造响应
return &types.GetAgentDownloadsResponse{
Total: stats.Total,
Platforms: &types.PlatformDownloads{
IOS: stats.IOS,
Android: stats.Android,
Windows: stats.Windows,
Mac: stats.Mac,
},
ComparisonRate: nil, // 不再计算环比
}, nil
}

View File

@ -0,0 +1,182 @@
package user
import (
"context"
"fmt"
"time"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/loki"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetAgentRealtimeLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetAgentRealtimeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAgentRealtimeLogic {
return &GetAgentRealtimeLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetAgentRealtimeLogic) GetAgentRealtime(req *types.GetAgentRealtimeRequest) (resp *types.GetAgentRealtimeResponse, err error) {
// 1. Get current user
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
l.Errorw("[GetAgentRealtime] user not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
var views, lastMonthViews int64
var installs int64
var paidCount int64
// 2. 从 Loki 获取 viewsnginx 访问日志)
lokiCfg := l.svcCtx.Config.Loki
if lokiCfg.Enable && lokiCfg.URL != "" && u.ReferCode != "" {
lokiClient := loki.NewClient(lokiCfg.URL)
lokiStats, err := lokiClient.GetInviteCodeStats(l.ctx, u.ReferCode, 30)
if err != nil {
l.Errorw("[GetAgentRealtime] Failed to fetch Loki stats",
logger.Field("error", err.Error()),
logger.Field("user_id", u.Id),
logger.Field("refer_code", u.ReferCode))
// 不返回错误,继续使用已有数据
} else {
views = lokiStats.MacClicks + lokiStats.WindowsClicks
lastMonthViews = lokiStats.LastMonthMac + lokiStats.LastMonthWindows
l.Infow("[GetAgentRealtime] Fetched Loki stats successfully",
logger.Field("user_id", u.Id),
logger.Field("refer_code", u.ReferCode),
logger.Field("views", views),
logger.Field("last_month_views", lastMonthViews))
}
}
// 3. 从数据库获取安装量(被邀请注册用户数)
err = l.svcCtx.DB.WithContext(l.ctx).
Model(&user.User{}).
Where("referer_id = ?", u.Id).
Count(&installs).Error
if err != nil {
l.Errorw("[GetAgentRealtime] Failed to count installs",
logger.Field("error", err.Error()),
logger.Field("user_id", u.Id))
installs = 0
}
// 4. 获取付费用户数
err = l.svcCtx.DB.WithContext(l.ctx).
Table("`order`").
Joins("LEFT JOIN user ON user.id = `order`.user_id").
Where("user.referer_id = ? AND `order`.status IN ?", u.Id, []int{2, 5}).
Distinct("`order`.user_id").
Count(&paidCount).Error
if err != nil {
l.Errorw("[GetAgentRealtime] Failed to count paid users",
logger.Field("error", err.Error()),
logger.Field("user_id", u.Id))
paidCount = 0
}
// 5. 计算环比增长率
growthRate := calculateGrowthRate([]int{int(lastMonthViews), int(views)})
// 6. 计算付费用户环比增长率
paidGrowthRate := l.calculatePaidGrowthRate(u.Id)
return &types.GetAgentRealtimeResponse{
Total: views,
Clicks: views,
Views: views,
Installs: installs,
PaidCount: paidCount,
GrowthRate: growthRate,
PaidGrowthRate: paidGrowthRate,
}, nil
}
// calculatePaidGrowthRate 计算付费用户的环比增长率
func (l *GetAgentRealtimeLogic) calculatePaidGrowthRate(userId int64) string {
db := l.svcCtx.DB
// 获取本月第一天和上月第一天
now := time.Now()
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
lastMonthStart := currentMonthStart.AddDate(0, -1, 0)
// 查询本月付费用户数(本月有新订单的)
var currentMonthCount int64
err := db.Table("`order` o").
Joins("JOIN user u ON o.user_id = u.id").
Where("u.referer_id = ? AND o.status IN (?, ?) AND o.created_at >= ?",
userId, 2, 5, currentMonthStart).
Distinct("o.user_id").
Count(&currentMonthCount).Error
if err != nil {
l.Errorw("Failed to count current month paid users",
logger.Field("error", err.Error()),
logger.Field("user_id", userId))
return "N/A"
}
// 查询上月付费用户数
var lastMonthCount int64
err = db.Table("`order` o").
Joins("JOIN user u ON o.user_id = u.id").
Where("u.referer_id = ? AND o.status IN (?, ?) AND o.created_at >= ? AND o.created_at < ?",
userId, 2, 5, lastMonthStart, currentMonthStart).
Distinct("o.user_id").
Count(&lastMonthCount).Error
if err != nil {
l.Errorw("Failed to count last month paid users",
logger.Field("error", err.Error()),
logger.Field("user_id", userId))
return "N/A"
}
// 计算增长率
return calculateGrowthRate([]int{int(lastMonthCount), int(currentMonthCount)})
}
// calculateGrowthRate 计算环比增长率
// views: 月份数据数组,最后一个是本月,倒数第二个是上月
func calculateGrowthRate(views []int) string {
if len(views) < 2 {
return "N/A"
}
currentMonth := views[len(views)-1]
lastMonth := views[len(views)-2]
// 如果上月是0无法计算百分比
if lastMonth == 0 {
if currentMonth == 0 {
return "0%"
}
return "+100%"
}
// 计算增长率
growth := float64(currentMonth-lastMonth) / float64(lastMonth) * 100
// 格式化输出
if growth > 0 {
return fmt.Sprintf("+%.1f%%", growth)
} else if growth < 0 {
return fmt.Sprintf("%.1f%%", growth)
}
return "0%"
}

View File

@ -0,0 +1,142 @@
package user
import (
"context"
"fmt"
"hash/fnv"
"strconv"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetInviteSalesLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetInviteSalesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInviteSalesLogic {
return &GetInviteSalesLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetInviteSalesLogic) GetInviteSales(req *types.GetInviteSalesRequest) (resp *types.GetInviteSalesResponse, err error) {
// 1. Get current user
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
l.Errorw("[GetInviteSales] user not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
userId := u.Id
// 2. Count total sales
var totalSales int64
db := l.svcCtx.DB.WithContext(l.ctx).
Table("`order` o").
Joins("JOIN user u ON o.user_id = u.id").
Where("u.referer_id = ? AND o.status = ?", userId, 5)
if req.StartTime > 0 {
db = db.Where("o.updated_at >= FROM_UNIXTIME(?)", req.StartTime)
}
if req.EndTime > 0 {
db = db.Where("o.updated_at <= FROM_UNIXTIME(?)", req.EndTime)
}
err = db.Count(&totalSales).Error
if err != nil {
l.Errorw("[GetInviteSales] count sales failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
"count sales failed: %v", err.Error())
}
// 3. Pagination
if req.Page < 1 {
req.Page = 1
}
if req.Size < 1 {
req.Size = 10
}
if req.Size > 100 {
req.Size = 100
}
offset := (req.Page - 1) * req.Size
// 4. Get sales data
type OrderWithUser struct {
Amount int64 `gorm:"column:amount"`
UpdatedAt int64 `gorm:"column:updated_at"`
UserId int64 `gorm:"column:user_id"`
ProductName string `gorm:"column:product_name"`
Quantity int64 `gorm:"column:quantity"`
}
var orderData []OrderWithUser
query := l.svcCtx.DB.WithContext(l.ctx).
Table("`order` o").
Select("o.amount, CAST(UNIX_TIMESTAMP(o.updated_at) * 1000 AS SIGNED) as updated_at, u.id as user_id, s.name as product_name, o.quantity").
Joins("JOIN user u ON o.user_id = u.id").
Joins("LEFT JOIN subscribe s ON o.subscribe_id = s.id").
Where("u.referer_id = ? AND o.status = ?", userId, 5) // status 5: Finished
if req.StartTime > 0 {
query = query.Where("o.updated_at >= FROM_UNIXTIME(?)", req.StartTime)
}
if req.EndTime > 0 {
query = query.Where("o.updated_at <= FROM_UNIXTIME(?)", req.EndTime)
}
err = query.Order("o.updated_at DESC").
Limit(req.Size).
Offset(offset).
Scan(&orderData).Error
if err != nil {
l.Errorw("[GetInviteSales] query sales failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
"query sales failed: %v", err.Error())
}
// 5. Get sales list
const HashSalt = "ppanel_invite_sales_v1" // Fixed Key
var list []types.InvitedUserSale
for _, order := range orderData {
// Calculate unique numeric hash (FNV-64a)
h := fnv.New64a()
h.Write([]byte(HashSalt))
h.Write([]byte(strconv.FormatInt(order.UserId, 10)))
// Truncate to 10 digits using modulo 10^10
hashVal := h.Sum64() % 10000000000
userHashStr := fmt.Sprintf("%010d", hashVal)
// Format product name as "{{ quantity }}天VPN服务"
productName := fmt.Sprintf("%d天VPN服务", order.Quantity)
if order.Quantity <= 0 {
productName = "1天VPN服务"
}
list = append(list, types.InvitedUserSale{
Amount: float64(order.Amount) / 100.0, // Convert cents to dollars
UpdatedAt: order.UpdatedAt,
UserHash: userHashStr,
ProductName: productName,
})
}
return &types.GetInviteSalesResponse{
Total: totalSales,
List: list,
}, nil
}

View File

@ -0,0 +1,168 @@
package user
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/perfect-panel/server/internal/config"
"github.com/perfect-panel/server/internal/model/order"
"github.com/perfect-panel/server/internal/model/subscribe"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// setupTestSvcCtx 初始化测试上下文
func setupTestSvcCtx(t *testing.T) (*svc.ServiceContext, *gorm.DB) {
// 1. Setup Miniredis
mr, err := miniredis.Run()
assert.NoError(t, err)
rdb := redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
// 2. Setup GORM with SQLite
dbName := fmt.Sprintf("test_sales_%d.db", time.Now().UnixNano())
db, err := gorm.Open(sqlite.Open(dbName), &gorm.Config{})
assert.NoError(t, err)
t.Cleanup(func() {
os.Remove(dbName)
mr.Close()
})
// Migrate tables
_ = db.Migrator().CreateTable(&user.User{})
_ = db.Migrator().CreateTable(&subscribe.Subscribe{}) // Plan definition
_ = db.Migrator().CreateTable(&order.Order{})
// 3. Create ServiceContext
svcCtx := &svc.ServiceContext{
Redis: rdb,
DB: db,
Config: config.Config{},
UserModel: user.NewModel(db, rdb),
}
return svcCtx, db
}
func TestGetInviteSales_TimeFilter(t *testing.T) {
svcCtx, db := setupTestSvcCtx(t)
// 1. Prepare Data
// Referrer User (Current User)
referrer := &user.User{
Id: 100,
// Email removed (not in struct)
ReferCode: "REF100",
}
db.Create(referrer)
// Invited User
invitedUser := &user.User{
Id: 200,
// Email removed
RefererId: referrer.Id, // Linked to referrer
}
db.Create(invitedUser)
// Subscribe (Plan)
sub := &subscribe.Subscribe{
Id: 1,
Name: "Standard Plan",
}
db.Create(sub)
// Orders
// Order 1: Inside Range (2023-10-15)
timeIn := time.Date(2023, 10, 15, 12, 0, 0, 0, time.UTC)
db.Create(&order.Order{
UserId: invitedUser.Id,
OrderNo: "ORD001",
Status: 5, // Finished
Amount: 1000,
Quantity: 30,
SubscribeId: sub.Id,
UpdatedAt: timeIn,
})
// Order 2: Before Range (2023-09-15)
timeBefore := time.Date(2023, 9, 15, 12, 0, 0, 0, time.UTC)
db.Create(&order.Order{
UserId: invitedUser.Id,
OrderNo: "ORD002",
Status: 5, // Finished
Amount: 1000,
Quantity: 30,
SubscribeId: sub.Id,
UpdatedAt: timeBefore,
})
// Order 3: After Range (2023-11-15)
timeAfter := time.Date(2023, 11, 15, 12, 0, 0, 0, time.UTC)
db.Create(&order.Order{
UserId: invitedUser.Id,
OrderNo: "ORD003",
Status: 5, // Finished
Amount: 1000,
Quantity: 30,
SubscribeId: sub.Id,
UpdatedAt: timeAfter,
})
// Order 4: Wrong Status (2023-10-16) - Should be ignored
db.Create(&order.Order{
UserId: invitedUser.Id,
OrderNo: "ORD004",
Status: 1, // Pending
Amount: 1000,
Quantity: 30,
SubscribeId: sub.Id,
UpdatedAt: timeIn.Add(24 * time.Hour),
})
// 2. Execute Logic
// Context with current user
ctx := context.WithValue(context.Background(), constant.CtxKeyUser, referrer)
l := NewGetInviteSalesLogic(ctx, svcCtx)
// Filter for October 2023
startTime := time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC).Unix()
endTime := time.Date(2023, 10, 31, 23, 59, 59, 0, time.UTC).Unix()
req := &types.GetInviteSalesRequest{
Page: 1,
Size: 10,
StartTime: startTime, // 2023-10-01
EndTime: endTime, // 2023-10-31
}
resp, err := l.GetInviteSales(req)
assert.NoError(t, err)
// 3. Verify Results
// Should match exactly 1 order (ORD001)
assert.Equal(t, int64(1), resp.Total, "Should return exactly 1 order matching time range and status")
if assert.NotEmpty(t, resp.List) {
assert.Equal(t, 1, len(resp.List))
// Log result for debug
t.Logf("Found Sale: Amount=%.2f, Time=%d", resp.List[0].Amount, resp.List[0].UpdatedAt)
// Verify timestamp is roughly correct (millisecond precision in logic)
expectedMs := timeIn.Unix() * 1000
assert.Equal(t, expectedMs, resp.List[0].UpdatedAt)
} else {
t.Error("Returned list is empty")
}
}

View File

@ -0,0 +1,78 @@
package user
import (
"context"
"database/sql"
"github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/internal/types"
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
)
type GetUserInviteStatsLogic struct {
logger.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
// Get user invite statistics
func NewGetUserInviteStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserInviteStatsLogic {
return &GetUserInviteStatsLogic{
Logger: logger.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUserInviteStatsLogic) GetUserInviteStats(req *types.GetUserInviteStatsRequest) (resp *types.GetUserInviteStatsResponse, err error) {
// 1. 从 context 中获取当前登录用户
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
if !ok {
l.Errorw("[GetUserInviteStats] user not found in context")
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
userId := u.Id
// 2. 获取历史邀请佣金 (FriendlyCount): 所有被邀请用户产生订单的佣金总和
// 注意:这里复用了 friendly_count 字段名,实际含义是佣金总额
var totalCommission sql.NullInt64
err = l.svcCtx.DB.WithContext(l.ctx).
Table("`order` o").
Select("COALESCE(SUM(o.commission), 0) as total").
Joins("JOIN user u ON o.user_id = u.id").
Where("u.referer_id = ? AND o.status IN (?, ?)", userId, 2, 5). // 只统计已支付和已完成的订单
Scan(&totalCommission).Error
if err != nil {
l.Errorw("[GetUserInviteStats] sum commission failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
"sum commission failed: %v", err.Error())
}
friendlyCount := totalCommission.Int64
// 3. 获取历史邀请总数 (HistoryCount)
var historyCount int64
err = l.svcCtx.DB.WithContext(l.ctx).
Table("user").
Where("referer_id = ?", userId).
Count(&historyCount).Error
if err != nil {
l.Errorw("[GetUserInviteStats] count history users failed",
logger.Field("error", err.Error()),
logger.Field("user_id", userId))
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError),
"count history users failed: %v", err.Error())
}
return &types.GetUserInviteStatsResponse{
FriendlyCount: friendlyCount,
HistoryCount: historyCount,
}, nil
}

View File

@ -60,6 +60,14 @@ func (l *QueryUserSubscribeLogic) QueryUserSubscribe() (resp *types.QueryUserSub
} }
} }
// 查询订单金额判断是否为赠送订单amount=0
if item.OrderId > 0 {
orderInfo, err := l.svcCtx.OrderModel.FindOne(l.ctx, item.OrderId)
if err == nil && orderInfo != nil {
sub.IsGift = orderInfo.Amount == 0
}
}
sub.ResetTime = calculateNextResetTime(&sub) sub.ResetTime = calculateNextResetTime(&sub)
resp.List = append(resp.List, sub) resp.List = append(resp.List, sub)
} }

View File

@ -126,8 +126,9 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
l.svcCtx.DeviceManager.KickDevice(u.Id, identifier) l.svcCtx.DeviceManager.KickDevice(u.Id, identifier)
// clean user cache // clean user cache
_ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u) _ = l.svcCtx.UserModel.ClearUserCache(l.ctx, u)
l.Infow("设备解绑完成", l.Infow("[SessionMonitor] 设备解绑触发 Session 清理",
logger.Field("device_identifier", identifier), logger.Field("device_identifier", identifier),
logger.Field("user_id", u.Id),
logger.Field("elapsed_ms", duration.Milliseconds())) logger.Field("elapsed_ms", duration.Milliseconds()))
return nil return nil
} }

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/config"
@ -37,6 +38,7 @@ type CacheKeyPayload struct {
} }
func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error { func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
req.Email = strings.ToLower(strings.TrimSpace(req.Email))
cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email) cacheKey := fmt.Sprintf("%s:%s:%s", config.AuthCodeCacheKey, constant.Security, req.Email)
value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result() value, err := l.svcCtx.Redis.Get(l.ctx, cacheKey).Result()
if err != nil { if err != nil {

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
jwtGo "github.com/golang-jwt/jwt/v5"
"github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/logger"
@ -36,7 +37,25 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
// parse token // parse token
claims, err := jwt.ParseJwtToken(token, jwtConfig.AccessSecret) claims, err := jwt.ParseJwtToken(token, jwtConfig.AccessSecret)
if err != nil { if err != nil {
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] parse token failed", logger.Field("error", err.Error())) logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] parse token failed",
logger.Field("error", err.Error()),
)
// [AuthDebug] Try to parse unverified to see why it failed (expired or invalid signature)
parser := jwtGo.NewParser()
unverifiedToken, _, _ := parser.ParseUnverified(token, jwtGo.MapClaims{})
if unverifiedToken != nil {
if unverifiedClaims, ok := unverifiedToken.Claims.(jwtGo.MapClaims); ok {
logger.WithContext(c.Request.Context()).Errorw("[AuthDebug] Token Parsing Failure Details",
logger.Field("exp", unverifiedClaims["exp"]),
logger.Field("iat", unverifiedClaims["iat"]),
logger.Field("uid", unverifiedClaims["UserId"]),
logger.Field("sid", unverifiedClaims["SessionId"]),
logger.Field("sub", unverifiedClaims["sub"]),
)
}
}
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.ErrorTokenExpire), "Token Invalid")) result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.ErrorTokenExpire), "Token Invalid"))
c.Abort() c.Abort()
return return
@ -60,9 +79,17 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
value, err := svc.Redis.Get(c, sessionIdCacheKey).Result() value, err := svc.Redis.Get(c, sessionIdCacheKey).Result()
if err != nil { if err != nil {
if errors.Is(err, redis.Nil) { if errors.Is(err, redis.Nil) {
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] session not found", logger.Field("sessionId", sessionId)) logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] Session无效或已过期",
logger.Field("session_id", sessionId),
logger.Field("user_id", userId),
logger.Field("redis_key", sessionIdCacheKey),
logger.Field("ip", c.ClientIP()),
logger.Field("path", c.Request.URL.Path))
} else { } else {
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] redis get failed", logger.Field("error", err.Error()), logger.Field("sessionId", sessionId)) logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] Redis 查询失败",
logger.Field("error", err.Error()),
logger.Field("session_id", sessionId),
logger.Field("redis_key", sessionIdCacheKey))
} }
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")) result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access"))
c.Abort() c.Abort()
@ -71,7 +98,10 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
//verify user id //verify user id
if value != fmt.Sprintf("%v", userId) { if value != fmt.Sprintf("%v", userId) {
logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] user mismatch", logger.Field("userId", userId), logger.Field("sessionId", sessionId), logger.Field("value", value)) logger.WithContext(c.Request.Context()).Errorw("[AuthMiddleware] user mismatch",
logger.Field("userId_in_token", userId),
logger.Field("userId_in_redis", value),
logger.Field("sessionId", sessionId))
result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")) result.HttpResult(c, nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access"))
c.Abort() c.Abort()
return return
@ -92,7 +122,17 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) {
c.Abort() c.Abort()
return return
} }
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] auth ok", logger.Field("userId", userId), logger.Field("loginType", loginType), logger.Field("path", c.Request.URL.Path)) // Get TTL details for debugging
ttl, _ := svc.Redis.TTL(c.Request.Context(), sessionIdCacheKey).Result()
logger.WithContext(c.Request.Context()).Infow("[AuthMiddleware] auth ok",
logger.Field("userId", userId),
logger.Field("loginType", loginType),
logger.Field("path", c.Request.URL.Path),
logger.Field("session_ttl", ttl.Seconds()),
logger.Field("sessionId", sessionId),
logger.Field("deviceId", deviceId),
)
ctx = context.WithValue(ctx, constant.LoginType, loginType) ctx = context.WithValue(ctx, constant.LoginType, loginType)
ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo)
ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId)

View File

@ -6,8 +6,10 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"regexp"
"strings" "strings"
model "github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/constant"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -31,6 +33,21 @@ func (w bodyLogWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b) return w.ResponseWriter.Write(b)
} }
// inviteCodeRegex matches invite code patterns in URLs like:
// /v1/common/client/download/file/Hi快VPN-mac-1.0.0-ic-uuSo11uy.dmg
// Matches: ic-XXXXX or ic_XXXXX before file extension
var inviteCodeRegex = regexp.MustCompile(`[-_]ic[-_]([a-zA-Z0-9]+)\.[a-zA-Z0-9]+$`)
// extractInviteCode extracts invite code from URL path
// Returns empty string if no invite code found
func extractInviteCode(path string) string {
matches := inviteCodeRegex.FindStringSubmatch(path)
if len(matches) >= 2 {
return matches[1]
}
return ""
}
// statusByWriter returns a span status code and message for an HTTP status code // statusByWriter returns a span status code and message for an HTTP status code
// value returned by a server. Status codes in the 400-499 range are not // value returned by a server. Status codes in the 400-499 range are not
// returned as errors. // returned as errors.
@ -48,7 +65,7 @@ func requestAttributes(req *http.Request) []attribute.KeyValue {
protoN := strings.SplitN(req.Proto, "/", 2) protoN := strings.SplitN(req.Proto, "/", 2)
remoteAddrN := strings.SplitN(req.RemoteAddr, ":", 2) remoteAddrN := strings.SplitN(req.RemoteAddr, ":", 2)
return []attribute.KeyValue{ attrs := []attribute.KeyValue{
semconv.HTTPRequestMethodKey.String(req.Method), semconv.HTTPRequestMethodKey.String(req.Method),
semconv.HTTPUserAgentKey.String(req.UserAgent()), semconv.HTTPUserAgentKey.String(req.UserAgent()),
semconv.HTTPRequestContentLengthKey.Int64(req.ContentLength), semconv.HTTPRequestContentLengthKey.Int64(req.ContentLength),
@ -65,6 +82,66 @@ func requestAttributes(req *http.Request) []attribute.KeyValue {
semconv.ClientAddressKey.String(remoteAddrN[0]), semconv.ClientAddressKey.String(remoteAddrN[0]),
semconv.ClientPortKey.String(remoteAddrN[1]), semconv.ClientPortKey.String(remoteAddrN[1]),
} }
// Extract invite code from URL path (e.g., /v1/common/client/download/file/Hi快VPN-mac-1.0.0-ic-uuSo11uy.dmg)
if inviteCode := extractInviteCode(req.URL.Path); inviteCode != "" {
attrs = append(attrs, attribute.String("affiliate.invite_code", inviteCode))
attrs = append(attrs, attribute.String("affiliate.source", "download_link"))
}
// Also check query parameter for invite code (e.g., ?ic=uuSo11uy)
if ic := req.URL.Query().Get("ic"); ic != "" {
attrs = append(attrs, attribute.String("affiliate.invite_code", ic))
attrs = append(attrs, attribute.String("affiliate.source", "query_param"))
}
return attrs
}
// userAttributes extracts user information from context and returns span attributes
func userAttributes(ctx context.Context) []attribute.KeyValue {
var attrs []attribute.KeyValue
// Get user info from context (set by authMiddleware)
if userInfo := ctx.Value(constant.CtxKeyUser); userInfo != nil {
if user, ok := userInfo.(*model.User); ok {
var email string
for _, method := range user.AuthMethods {
if method.AuthType == "email" {
email = method.AuthIdentifier
break
}
}
attrs = append(attrs,
attribute.Int64("user.id", user.Id),
attribute.String("user.email", email),
attribute.Bool("user.is_admin", *user.IsAdmin),
)
}
}
// Get session ID from context
if sessionID := ctx.Value(constant.CtxKeySessionID); sessionID != nil {
if sid, ok := sessionID.(string); ok {
attrs = append(attrs, attribute.String("user.session_id", sid))
}
}
// Get device ID from context
if deviceID := ctx.Value(constant.CtxKeyDeviceID); deviceID != nil {
if did, ok := deviceID.(int64); ok {
attrs = append(attrs, attribute.Int64("user.device_id", did))
}
}
// Get login type from context
if loginType := ctx.Value(constant.LoginType); loginType != nil {
if lt, ok := loginType.(string); ok {
attrs = append(attrs, attribute.String("user.login_type", lt))
}
}
return attrs
} }
func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) { func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
@ -99,6 +176,9 @@ func TraceMiddleware(_ *svc.ServiceContext) func(ctx *gin.Context) {
semconv.HTTPRouteKey.String(c.FullPath()), semconv.HTTPRouteKey.String(c.FullPath()),
) )
// Add user attributes from context (set by authMiddleware)
span.SetAttributes(userAttributes(ctx)...)
// Record Request Body (limit to 1MB) // Record Request Body (limit to 1MB)
if len(reqBody) > 0 { if len(reqBody) > 0 {
limit := 1048576 limit := 1048576

View File

@ -249,7 +249,7 @@ func (m *customOrderModel) IsUserEligibleForNewOrder(ctx context.Context, userID
var count int64 var count int64
err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error { err := m.QueryNoCacheCtx(ctx, nil, func(conn *gorm.DB, _ interface{}) error {
return conn.Model(&Order{}). return conn.Model(&Order{}).
Where("user_id = ? AND status IN ?", userID, []int64{2, 5}). Where("user_id = ? AND status IN ? AND amount > 0", userID, []int64{2, 5}).
Count(&count).Error Count(&count).Error
}) })
return count == 0, err return count == 0, err

View File

@ -42,7 +42,7 @@ type Subscribe struct {
User User `gorm:"foreignKey:UserId;references:Id"` User User `gorm:"foreignKey:UserId;references:Id"`
OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"` OrderId int64 `gorm:"index:idx_order_id;not null;comment:Order ID"`
SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"` SubscribeId int64 `gorm:"index:idx_subscribe_id;not null;comment:Subscription ID"`
StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP(3);not null;comment:Subscription Start Time"` StartTime time.Time `gorm:"default:CURRENT_TIMESTAMP;not null;comment:Subscription Start Time"`
ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"` ExpireTime time.Time `gorm:"default:NULL;comment:Subscription Expire Time"`
FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"` FinishedAt *time.Time `gorm:"default:NULL;comment:Finished Time"`
Traffic int64 `gorm:"default:0;comment:Traffic"` Traffic int64 `gorm:"default:0;comment:Traffic"`

View File

@ -13,7 +13,6 @@ import (
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis" "github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/perfect-panel/server/initialize"
"github.com/perfect-panel/server/internal/handler" "github.com/perfect-panel/server/internal/handler"
"github.com/perfect-panel/server/internal/middleware" "github.com/perfect-panel/server/internal/middleware"
"github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/svc"
@ -32,8 +31,6 @@ func NewService(svc *svc.ServiceContext) *Service {
func initServer(svc *svc.ServiceContext) *gin.Engine { func initServer(svc *svc.ServiceContext) *gin.Engine {
// start init system config
initialize.StartInitSystemConfig(svc)
// init gin server // init gin server
r := gin.Default() r := gin.Default()
r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"} r.RemoteIPHeaders = []string{"X-Original-Forwarded-For", "X-Forwarded-For", "X-Real-IP"}

View File

@ -18,6 +18,7 @@ import (
"github.com/perfect-panel/server/internal/model/auth" "github.com/perfect-panel/server/internal/model/auth"
"github.com/perfect-panel/server/internal/model/coupon" "github.com/perfect-panel/server/internal/model/coupon"
"github.com/perfect-panel/server/internal/model/document" "github.com/perfect-panel/server/internal/model/document"
iapapple "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/internal/model/log" "github.com/perfect-panel/server/internal/model/log"
logmessage "github.com/perfect-panel/server/internal/model/logmessage" logmessage "github.com/perfect-panel/server/internal/model/logmessage"
"github.com/perfect-panel/server/internal/model/order" "github.com/perfect-panel/server/internal/model/order"
@ -27,7 +28,6 @@ import (
"github.com/perfect-panel/server/internal/model/ticket" "github.com/perfect-panel/server/internal/model/ticket"
"github.com/perfect-panel/server/internal/model/traffic" "github.com/perfect-panel/server/internal/model/traffic"
"github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/model/user"
iapapple "github.com/perfect-panel/server/internal/model/iap/apple"
"github.com/perfect-panel/server/pkg/limit" "github.com/perfect-panel/server/pkg/limit"
"github.com/perfect-panel/server/pkg/nodeMultiplier" "github.com/perfect-panel/server/pkg/nodeMultiplier"
"github.com/perfect-panel/server/pkg/orm" "github.com/perfect-panel/server/pkg/orm"
@ -74,10 +74,12 @@ type ServiceContext struct {
func NewServiceContext(c config.Config) *ServiceContext { func NewServiceContext(c config.Config) *ServiceContext {
// gorm initialize // gorm initialize
fmt.Printf(" [Debug] MySQL Config -> Addr: %s, User: %s, DB: %s\n", c.MySQL.Addr, c.MySQL.Username, c.MySQL.Dbname)
db, err := orm.ConnectMysql(orm.Mysql{ db, err := orm.ConnectMysql(orm.Mysql{
Config: c.MySQL, Config: c.MySQL,
}) })
if err != nil { if err != nil {
fmt.Printf(" [Debug] Connection Error: %v\n", err)
panic(err.Error()) panic(err.Error())
} }
rds := redis.NewClient(&redis.Options{ rds := redis.NewClient(&redis.Options{
@ -126,6 +128,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
} }
func (srv *ServiceContext) SessionLimit() int64 { func (srv *ServiceContext) SessionLimit() int64 {
// check custom data
cd := srv.Config.Site.CustomData cd := srv.Config.Site.CustomData
if cd != "" { if cd != "" {
var obj map[string]interface{} var obj map[string]interface{}
@ -134,10 +137,12 @@ func (srv *ServiceContext) SessionLimit() int64 {
switch val := v.(type) { switch val := v.(type) {
case float64: case float64:
if val > 0 { if val > 0 {
fmt.Printf("[SessionLimit] Using CustomData deviceLimit: %d\n", int64(val))
return int64(val) return int64(val)
} }
case string: case string:
if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 { if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 {
fmt.Printf("[SessionLimit] Using CustomData deviceLimit: %d\n", n)
return n return n
} }
} }
@ -146,16 +151,19 @@ func (srv *ServiceContext) SessionLimit() int64 {
switch val := v.(type) { switch val := v.(type) {
case float64: case float64:
if val > 0 { if val > 0 {
fmt.Printf("[SessionLimit] Using CustomData DeviceLimit: %d\n", int64(val))
return int64(val) return int64(val)
} }
case string: case string:
if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 { if n, err := strconv.ParseInt(strings.TrimSpace(val), 10, 64); err == nil && n > 0 {
fmt.Printf("[SessionLimit] Using CustomData DeviceLimit: %d\n", n)
return n return n
} }
} }
} }
} }
} }
fmt.Printf("[SessionLimit] Using Config MaxSessionsPerUser: %d\n", srv.Config.JwtAuth.MaxSessionsPerUser)
return srv.Config.JwtAuth.MaxSessionsPerUser return srv.Config.JwtAuth.MaxSessionsPerUser
} }
@ -173,6 +181,22 @@ func (srv *ServiceContext) EnforceUserSessionLimit(ctx context.Context, userId i
return err return err
} }
if count > max { if count > max {
// [SessionDebug] Log all current sessions before eviction
// Fetch all sessions (oldest to newest)
sessions, _ := srv.Redis.ZRange(ctx, sessionsKey, 0, -1).Result()
fmt.Printf("[SessionMonitor] ⚠️ Session Limit Exceeded (Count: %d, Max: %d). User %d has the following active sessions:\n", count, max, userId)
for i, sid := range sessions {
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)
val, err := srv.Redis.Get(ctx, detailKey).Result()
if err == nil {
fmt.Printf(" [%d] SessionID: %s | Detail: %s\n", i+1, sid, val)
} else {
fmt.Printf(" [%d] SessionID: %s | (No Detail - Likely old session)\n", i+1, sid)
}
}
// Log before eviction
popped, err := srv.Redis.ZPopMin(ctx, sessionsKey, count-max).Result() popped, err := srv.Redis.ZPopMin(ctx, sessionsKey, count-max).Result()
if err != nil { if err != nil {
return err return err
@ -180,6 +204,12 @@ func (srv *ServiceContext) EnforceUserSessionLimit(ctx context.Context, userId i
for _, z := range popped { for _, z := range popped {
sid := fmt.Sprintf("%v", z.Member) sid := fmt.Sprintf("%v", z.Member)
_ = srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sid)).Err() _ = srv.Redis.Del(ctx, fmt.Sprintf("%v:%v", config.SessionIdKey, sid)).Err()
// Also delete detail
_ = srv.Redis.Del(ctx, fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)).Err()
// 记录被踢出的 Session 信息
fmt.Printf("[SessionMonitor] ❌ KICKED OUT Session: user_id=%d session_id=%s reason=exceed_limit\n",
userId, sid)
} }
} }
_ = srv.Redis.Expire(ctx, sessionsKey, time.Duration(srv.Config.JwtAuth.AccessExpire)*time.Second).Err() _ = srv.Redis.Expire(ctx, sessionsKey, time.Duration(srv.Config.JwtAuth.AccessExpire)*time.Second).Err()

View File

@ -906,6 +906,67 @@ type GetGlobalConfigResponse struct {
WebAd bool `json:"web_ad"` WebAd bool `json:"web_ad"`
} }
type GetAgentRealtimeRequest struct{}
type GetAgentRealtimeResponse struct {
Total int64 `json:"total"` // 访问总人数
Clicks int64 `json:"clicks"` // 点击量
Views int64 `json:"views"` // 浏览量
Installs int64 `json:"installs"` // 安装量(被邀请注册用户数)
PaidCount int64 `json:"paid_count"` // 付费数量
GrowthRate string `json:"growth_rate"` // 访问量环比增长率
PaidGrowthRate string `json:"paid_growth_rate"` // 付费用户环比增长率
}
type GetAgentDownloadsRequest struct{}
type GetAgentDownloadsResponse struct {
Total int64 `json:"total"` // 总下载量
Platforms *PlatformDownloads `json:"platforms"` // 各平台下载量
ComparisonRate *string `json:"comparison_rate,omitempty"` // 与上月环比(如 "+15.5%" 或 "-10.0%"
}
// PlatformDownloads 各平台下载量统计
type PlatformDownloads struct {
IOS int64 `json:"ios"` // iPhone/iPad
Android int64 `json:"android"` // Android
Windows int64 `json:"windows"` // Windows
Mac int64 `json:"mac"` // Mac
}
// Deprecated: 旧的响应结构,保留以兼容
type AgentDownloadStats struct {
Platform string `json:"platform"`
Clicks int64 `json:"clicks"`
Visits int64 `json:"visits"`
}
type GetUserInviteStatsRequest struct{}
type GetUserInviteStatsResponse struct {
FriendlyCount int64 `json:"friendly_count"` // 有效邀请数(有订单的用户)
HistoryCount int64 `json:"history_count"` // 历史邀请总数
}
type GetInviteSalesRequest struct {
Page int `form:"page" validate:"required"`
Size int `form:"size" validate:"required"`
StartTime int64 `form:"start_time,optional"`
EndTime int64 `form:"end_time,optional"`
}
type GetInviteSalesResponse struct {
Total int64 `json:"total"` // 销售记录总数
List []InvitedUserSale `json:"list"` // 销售数据列表(分页)
}
type InvitedUserSale struct {
Amount float64 `json:"amount"`
UpdatedAt int64 `json:"update_at"`
UserHash string `json:"user_hash"`
ProductName string `json:"product_name"`
}
type GetLoginLogRequest struct { type GetLoginLogRequest struct {
Page int `form:"page"` Page int `form:"page"`
Size int `form:"size"` Size int `form:"size"`
@ -2566,7 +2627,7 @@ type UpdateUserBasiceInfoRequest struct {
GiftAmount int64 `json:"gift_amount"` GiftAmount int64 `json:"gift_amount"`
Telegram int64 `json:"telegram"` Telegram int64 `json:"telegram"`
ReferCode string `json:"refer_code"` ReferCode string `json:"refer_code"`
RefererId int64 `json:"referer_id"` RefererId *int64 `json:"referer_id"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
IsAdmin bool `json:"is_admin"` IsAdmin bool `json:"is_admin"`
MemberStatus string `json:"member_status"` MemberStatus string `json:"member_status"`
@ -2728,6 +2789,7 @@ type UserSubscribe struct {
Upload int64 `json:"upload"` Upload int64 `json:"upload"`
Token string `json:"token"` Token string `json:"token"`
Status uint8 `json:"status"` Status uint8 `json:"status"`
IsGift bool `json:"is_gift"` // 是否为赠送订单amount=0
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"` UpdatedAt int64 `json:"updated_at"`
} }

398
log.txt Normal file
View File

@ -0,0 +1,398 @@
root@localhost7701:~# docker logs -f ppanel-server 2>&1 | grep -E "\[SessionMonitor\]|session_ttl|reusing existing session"
262618-01-01 00:00:00.478 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=603838 span=6136f5a8a1cb8b97 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=3006ee9b804ff8fb602f18cfd7dcf8d6
262618-01-01 00:00:00.668 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=603838 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=489c0fcd8ebf0082c93841ab80b467ad span=bcc06cc438370968
262626-01-01 00:00:00.351 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=601918 trace=90336bc9417ebbbb0e414036ed56d353 span=907f06fa6d5c33c6 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.495 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=601918 trace=4f05d07e2b2a62035dc7c51b621e9e9d sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=874e467e556daae9 userId=611 loginType=device path=/v1/public/user/subscribe
262629-01-01 00:00:00.003 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=603827 trace=1c942585c613f216399a86aecac83ecc span=9da277380f311e4d userId=599 path=/v1/public/subscribe/list sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262629-01-01 00:00:00.003 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=e30ee979308cab7b userId=599 path=/v1/public/user/subscribe trace=f55ecbfc8212303bb9c27fe084635c15 loginType=device session_ttl=603827 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262629-01-01 00:00:00.005 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=603827 span=eb2ab58b44f8dfc1 loginType=device path=/v1/public/user/info sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=b10c017f6f784fa536985223c93ac7bd
262629-01-01 00:00:00.238 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/payment/methods session_ttl=603827 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=6e05889f542e23230ca97858a1653abb span=987460d6b917b906 userId=599
262618-01-01 00:00:00.575 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=603778 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=f979e0f4f966fb917464c9a9255621cc userId=599 loginType=device span=cd7a250671a2d1dd
262618-01-01 00:00:00.755 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 loginType=device session_ttl=603777 trace=61912bd45a508bc5ca1eb7dd6d3a393d span=b3aaece90fe3611f
262626-01-01 00:00:00.367 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=3d7d8730ee3b332e602b983c763503fd loginType=device session_ttl=601858 span=71a2731b7af3bed4 userId=611
262626-01-01 00:00:00.521 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=f092eca8110717d4148570b1d9cf3722 userId=611 path=/v1/public/user/subscribe session_ttl=601858 span=7fff71c8218cf44e
262618-01-01 00:00:00.646 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=04c2d68005852782 loginType=device trace=64b2c6257326269d2fc06ecdcbaa6cde userId=599 path=/v1/public/user/subscribe session_ttl=603718
262618-01-01 00:00:00.830 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=234f119abdd485f7ce401ceda5bdcfae path=/v1/public/user/subscribe session_ttl=603717 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=d5f20cf98a8d26b0 userId=599 loginType=device
262626-01-01 00:00:00.394 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device span=caefba66206dbb87 path=/v1/public/user/subscribe session_ttl=601798 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=51bb10487c8b9acb0dcc2ced5f5d1ce3
262626-01-01 00:00:00.541 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 session_ttl=601798 trace=378205815808370e1df24ae30053c019 span=78fea7ef9dd8581c loginType=device
262618-01-01 00:00:00.764 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=ddf445d8ba1a6bd84570842809889361 loginType=device session_ttl=603657 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=c14b0c035f234d41 userId=599
262618-01-01 00:00:00.957 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=fd4cc7ed8c2e61bf userId=599 loginType=device session_ttl=603657 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=0ae88ca57b9539a5d11e4e1707076521 path=/v1/public/user/subscribe
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=d8ba24721d43aa70 loginType=device session_ttl=601738 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 path=/v1/public/user/subscribe trace=97d4616e81458218f3169826396cde2b
262626-01-01 00:00:00.526 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=b4c83943883f27eb userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=601738 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=3969bb74119b6289d11b3a15179ce2cf
262618-01-01 00:00:00.801 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=01c7c18c3c0eb8f2ce367b7282992ed0 session_ttl=603597 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=fdecb7138e548f36 userId=599
262618-01-01 00:00:00.994 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=603597 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 trace=e5e3258bdfbfc7b42569eb7d32d3f9fd span=eb200235c77edba7
262626-01-01 00:00:00.364 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=601678 trace=27132cacd76cc6581623627961f30aa9 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=91bc301e83dbc525
262626-01-01 00:00:00.514 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=ab3761183501f037 loginType=device session_ttl=601678 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 path=/v1/public/user/subscribe trace=fffc9e869f2c2196faac52abb0a47210
262618-01-01 00:00:00.746 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=59a9e37a5058e1154f8f99d141df3f61 userId=599 session_ttl=603537 span=30864a5c5763e603
262618-01-01 00:00:00.994 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=f3034a697252378b userId=599 session_ttl=603537 trace=d45edf4280a63e50b903b4444d0d69e9
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=601618 trace=c164a4df4859c61fc790a44360d7ff97 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=4e10c1b1d4b90863
262626-01-01 00:00:00.522 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device trace=3428ec0405a22cfd8cd0c9e51c41021e userId=611 path=/v1/public/user/subscribe session_ttl=601618 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=f45151c8ce603a28
262623-01-01 00:00:00.413 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=603473 span=47c7b88366ab8b5f path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=be54c7d5d40e1c582b2f72b3b61d09c7
262625-01-01 00:00:00.133 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=603471 trace=ea4be7d97fb25df161106fbdef12fdf4 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=4c73e31e1509e833
262626-01-01 00:00:00.368 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=601558 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=211b168efde0cbba loginType=device trace=a6f5c3d62ea269d4c8a1268002351fa2 userId=611
262626-01-01 00:00:00.511 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=ab2d45887589e99f413ae6ad4ebc90a7 path=/v1/public/user/subscribe session_ttl=601558 span=cbdcd656ff3212c6
262618-01-01 00:00:00.777 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 span=13bdd1cd318c9c9c loginType=device path=/v1/public/user/subscribe session_ttl=603417 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=416ac6c5acd881ca4cf982a3a031e01b
262618-01-01 00:00:00.957 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=603417 span=fcbb652bf7f49eba userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=74631d4673c1c096abf13079238eb201
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=86cf65398a88b0fe userId=611 loginType=device trace=865672206c8fd735ae4a9266447b03c0 path=/v1/public/user/subscribe session_ttl=601498
262626-01-01 00:00:00.528 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=601498 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=8362fe8ad6da1ad0 loginType=device trace=82349e2ea52752fc0aaab389a4cf7878
262618-01-01 00:00:00.396 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device trace=c510eb9e6de20fb1de9651b37490eaf8 span=b64fc1f15a500439 userId=599 path=/v1/public/user/subscribe session_ttl=603358 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.852 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=603357 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=4fbe772b69d85cbf userId=599 loginType=device trace=3a9092de7c742cdbb106a63861e6ef96 path=/v1/public/user/subscribe
262626-01-01 00:00:00.373 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=601438 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=37d18c340744e85a trace=32a96f4084ad36865a6c855897a9a72f
262626-01-01 00:00:00.522 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe loginType=device session_ttl=601438 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=9649a654fdf902a21505d8a99af0b31e span=3b7e477671d46256
262618-01-01 00:00:00.449 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=603298 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=a9fd1a37f8c9cb6e207896034897c40b span=ce9633e5a3d52fff
262618-01-01 00:00:00.959 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=4cee995928423364989e3f54a85fa8d8 path=/v1/public/user/subscribe session_ttl=603297 span=ad92c0eced1ba706
262626-01-01 00:00:00.368 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=2ef84f301e926410 userId=611 loginType=device session_ttl=601378 trace=96b4ef0946b97f62c8e1a36175ae5f8f path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.518 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=efa4048c295e6800 userId=611 loginType=device session_ttl=601378 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=f95f7a2f777b0c04e33509e521d729b8 path=/v1/public/user/subscribe
262618-01-01 00:00:00.408 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=603238 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=475fe1429e6385159561c69a6162ce08 span=e3615fccaf686147
262618-01-01 00:00:00.590 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=603238 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=33f6376ad35c9dbc loginType=device path=/v1/public/user/subscribe trace=6b911a1fa2eb3229a16da9bcde519cf9
262626-01-01 00:00:00.388 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=601318 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 trace=bb5bfe1af389e201c79b71d8c89bba5c span=a4f42379b5f4c338 loginType=device
262626-01-01 00:00:00.545 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=601318 trace=cb8f8534743022d3c145ff7ff33af939 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=08ecf3b2555418fe
262618-01-01 00:00:00.437 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 path=/v1/public/user/subscribe trace=451d894a95f6387646acc7009b2aa9d3 span=9ab35bfb051302d0 loginType=device session_ttl=603178
262618-01-01 00:00:00.636 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=4f1258d4222aa03b userId=599 loginType=device session_ttl=603178 trace=1ac3cf5c523a0e1a19052e850d478a9e
262626-01-01 00:00:00.694 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=5dbcfd3ee9cae2e0404605d8f2e2947f path=/v1/public/user/subscribe session_ttl=601258 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=88808755ef6182e6 userId=611 loginType=device
262626-01-01 00:00:00.963 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=601258 span=113325a79632095b sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=ad9b657c5a9238d1293eca100ad31072
262618-01-01 00:00:00.376 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=612effc332fd6dc84bb8dc775ef04fa5 span=c98942b84a133b3c session_ttl=603118 userId=599 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.574 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=603118 trace=204a09aa42aafd8c136e01524a54a123 span=b8fc8b59d630bb88 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599
262626-01-01 00:00:00.369 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=4daf6b9b08878ff5 session_ttl=601198 trace=d6b1cc64e62949e699cc260d445702a3 userId=611
262626-01-01 00:00:00.515 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=8975c0b4f89b222cba95921757c36c87 userId=611 loginType=device session_ttl=601198 span=5b303d85d0d84d3a
262618-01-01 00:00:00.680 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=603058 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a path=/v1/public/user/subscribe trace=18056534411d48924f5ba6b5bc5209a0 span=0720eacc57087db2 userId=599
262618-01-01 00:00:00.877 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device span=9ff23ae63ad2f0dauserId=599 path=/v1/public/user/subscribe session_ttl=603057 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=a2e8f94b0c1761fbfdf7bee391f9bf54
262626-01-01 00:00:00.347 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=601138 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=e5940716d23536ac loginType=device trace=fef4f6973c68242ddff7e87930316348 userId=611 path=/v1/public/user/subscribe
262626-01-01 00:00:00.493 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=aa77723d198428b66793d254f9c7a53e span=db43256d6a33fd6f session_ttl=601138
262618-01-01 00:00:00.397 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 session_ttl=602998 trace=937b3b42590d6eb759911b428043a8a1 span=314f28dbff014ac5
262618-01-01 00:00:00.589 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=602998 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=d72f434f0f705c9e7fa8d6e29482a0b5 userId=599 loginType=device span=4640db37837e2d33
262626-01-01 00:00:00.388 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=601078 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=316165eedb03882df25b5d2699a2f468 userId=611 path=/v1/public/user/subscribe span=a95fb3d3c2b24366
262626-01-01 00:00:00.538 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=cd57b5430d607cb374ed36f5b4e3b08b span=3c7e93a73714a786 userId=611 loginType=device session_ttl=601078 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
26262-01-01 00:00:00.239 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=641 session_ttl=604800 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 trace=c5324770af9d3f7358635dc217abfe9e span=d1a75bf344f2f245 loginType=device path=/v1/public/user/info
26262-01-01 00:00:00.621 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=efcc4c0034ea48d66d2f7d9efe33bec4 span=6fd30d0b815e3150 loginType=device session_ttl=604800 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 userId=641 path=/v1/public/user/subscribe
26262-01-01 00:00:00.648 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=641 loginType=device path=/v1/public/user/subscribe sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 session_ttl=604800 trace=06999edf0803f5d8a3342557a9e0c010 span=994c27bd524b7cea
26262-01-01 00:00:00.670 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/subscribe/node/list session_ttl=604800 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 trace=c16f79db59acf4f6dbdd89df1fd4dc80 userId=641 span=cf68906025c1be2b
262618-01-01 00:00:00.398 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 trace=465aa0f821ed02159a7c72eaacc34614 span=79d4f37505d037d2 loginType=device path=/v1/public/user/subscribe session_ttl=602938 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.591 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=602938 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=61b63379e8743587a19a6c89cf4e4cad path=/v1/public/user/subscribe span=bc36b8135553f9b1
262626-01-01 00:00:00.371 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=601018 trace=2c4af5ef4c20750ba196c1856183c455 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=4cfb9724088a54e4
262626-01-01 00:00:00.515 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=601018 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=5a614d0c8e6434417458026cd92a8131 span=d1b02fa0cd781563
262635-01-01 00:00:00.289 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=a88d60ecdb493043 userId=641 path=/v1/public/announcement/list sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 loginType=device session_ttl=604767 trace=fc3a5d1fca8a927700d37ae0b0f8c466
262636-01-01 00:00:00.760 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=641 path=/v1/public/user/affiliate/count session_ttl=604765 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 trace=6d8397b16eada46d0d72b334c21b5bf3 span=3baa6b0d7bfcc4b0 loginType=device
262657-01-01 00:00:00.064 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=641 loginType=device path=/v1/public/user/devices session_ttl=604745 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 trace=24b2580a9cf6f6309cc5eacfef76be84 span=2495f2e20391e23c
262658-01-01 00:00:00.814 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/devices session_ttl=604743 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 trace=466c88d4218bf0c17ed53a39ad181725 userId=641 span=072738014eea3c25
26262-01-01 00:00:00.746 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=b13befc57652a5fccc16dcd7977c910b loginType=device path=/v1/public/user/subscribe span=244276c195143582 userId=641 session_ttl=604739 sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18
26262-01-01 00:00:00.766 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device sessionId=019bfa28-4699-752b-ae2e-696c8c3ede18 path=/v1/public/user/subscribe session_ttl=604739 trace=3b7d5e23ce7fda06f16d2c8c55456e70 span=7b9260486b012cbe userId=641
262618-01-01 00:00:00.362 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=602878 trace=f21eea979b641302c25cc5da3476bc15 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=143c5271d8d0e6f6 userId=599
262618-01-01 00:00:00.557 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=602878 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device trace=484877271d5f1154d8c347e7a992f373 span=0b10fffe18c80b98
262626-01-01 00:00:00.406 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=600958 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=85238775424d28059cbcf4c8eae31673 path=/v1/public/user/subscribe span=a21b532a4ad1fbb3
262626-01-01 00:00:00.561 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=10c934779c77bbc862dd282a937da5ec span=c74e735d88f1a18f session_ttl=600958
262628-01-01 00:00:00.352 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=595357 sessionId=019bf952-9e35-752b-9881-8428a1a5b152 userId=441 path=/v1/public/user/subscribe trace=ea037596aa7af0725966b649b857cb00 span=124817eb09344f6c loginType=device
262629-01-01 00:00:00.043 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=595357 trace=d4697c10bda25df01130a1894c90cfea span=5fec024e068f6edd userId=441 sessionId=019bf952-9e35-752b-9881-8428a1a5b152
26267-01-01 00:00:00.451 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=a50708d46550b961 userId=441 loginType=device session_ttl=595318 sessionId=019bf952-9e35-752b-9881-8428a1a5b152 trace=67421935e72476e0f3a7410037688ca1 path=/v1/public/user/subscribe
26268-01-01 00:00:00.212 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=3a08258a796a6fe2320a31abf8a89b2d userId=441 path=/v1/public/user/subscribe sessionId=019bf952-9e35-752b-9881-8428a1a5b152 span=cae5249480999d2d loginType=device session_ttl=595317
262618-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=1fc58f3019f57c09 userId=599 loginType=device session_ttl=602818 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=b88882ff4d6dacbc8bd2a98f541ee420 path=/v1/public/user/subscribe
262618-01-01 00:00:00.564 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=602818 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=f397ee3f662a50b04ff1d17f704663ff span=e9a6979f17980cc0
262626-01-01 00:00:00.364 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe trace=73b4dcfbc40a84988ee143ffdcad4102 loginType=device session_ttl=600898 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=164654f7c165567e
262626-01-01 00:00:00.513 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=6db072dab3548749 path=/v1/public/user/subscribe session_ttl=600898 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=cf4ee69daf0d02a1c88606231bca2b51
262618-01-01 00:00:00.364 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=602758 userId=599 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c79328ef752193a77cbfaffcacaab682 span=19a0681a545204d0
262618-01-01 00:00:00.558 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=602758 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=0fc4cd79dbce356d34fade1b8ff1411a span=eb425c0b907945ca loginType=device
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=84520863a3c88724 trace=1dca302b54513988eefc3aff4da114f7 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600838 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.524 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=600838 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 path=/v1/public/user/subscribe trace=8aa222f2f0cc4f83a9a99553ba6ad12d span=635aeb92b055b339
262618-01-01 00:00:00.361 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=602698 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=561bcd95512d5a84f90c4d390c95331e span=a8f4eb710c425858 userId=599 path=/v1/public/user/subscribe
262618-01-01 00:00:00.557 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=602698 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=265fc60e0bf932ece3e91a9bee033c4e userId=599 span=48839ec6e71996f2
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=23796de03dc679612cc84717211a3f4d span=49e549fcd4862ecf userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600778 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.525 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600778 trace=e1fd274b89e0e3b45be84acbd5bf6a69 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=4f2ba84c29cf4a4f
262618-01-01 00:00:00.361 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe trace=b6a59a4bf6460e40d767318c0faed48e session_ttl=602638 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=66a562f58c1e61d4
262618-01-01 00:00:00.557 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=aa9808b1ba7e14de5beb147cb8a5183d span=986e30a61fb6e9b6 loginType=device path=/v1/public/user/subscribe session_ttl=602638
262626-01-01 00:00:00.367 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600718 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=8f2f9d05981837f66e4c652b38ca0cc3 span=b6f9a96aef0a3651
262626-01-01 00:00:00.510 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=a62748e6bd3f7afe38626699359fa84c span=88825e01b317d4ea loginType=device path=/v1/public/user/subscribe session_ttl=600718
262618-01-01 00:00:00.431 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=602578 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=937cbbb21e95c722 userId=599 loginType=device path=/v1/public/user/subscribe trace=f6e9e11f55eee3ad043448b3f5d52c70
262618-01-01 00:00:00.638 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=602578 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=57dbf9cb2062d977e253184cea5b1c5f span=9e6c670949a700bc
262626-01-01 00:00:00.365 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device trace=25e4906006ad84fd3febe857d036a514 span=e6b2661ec178a4d3 userId=611 path=/v1/public/user/subscribe session_ttl=600658 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.511 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=c08402a6744f6dff9eb2ab530396d7ef span=7465c9179c8dd93b session_ttl=600658
262618-01-01 00:00:00.405 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=602518 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=3e7e013b9ec3a635 userId=599 path=/v1/public/user/subscribe trace=abee617757483b29610baebf7bdad1c4
262618-01-01 00:00:00.585 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=b730998951cf6fe99fed84ac7761112c span=f813928a640cb320 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=602518
262626-01-01 00:00:00.347 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=600598 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=3c56c9ea8c44648c242b27bca317b6a9 userId=611 span=29e78155acbbc44a
262626-01-01 00:00:00.488 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=8bb4e0ba0ed50755 loginType=device path=/v1/public/user/subscribe trace=a19f46bef15fa1ea2effa42f8cc059e0 userId=611 session_ttl=600598 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.407 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=034c39d086ee8460 path=/v1/public/user/subscribe session_ttl=602458 trace=c88b965b5ef1740b775c893bd6df1324 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.591 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe trace=778acf6505378b52425ffb62eb1ed3d8 span=21c1aceff979521f loginType=device session_ttl=602458 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.359 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=f2a5b56949c015506facee7d4bc3ba30 span=9b25631829336b5c userId=611 session_ttl=600538 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.508 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=600538 trace=82176bbf105f1008894cbb33c7dbd963 span=85181a1989609ba2 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.358 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=d97d95f130794eaf userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device path=/v1/public/user/subscribe session_ttl=602398 trace=cf19b0175f709079e1c3daaf8cf02bec
262618-01-01 00:00:00.557 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=602398 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=66db3d1781a7fc733334230f3ecb72a4 span=2523f5c4d8bd8f28
262626-01-01 00:00:00.381 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=600478 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=3496a360f858f22cff0e98bba2f439be userId=611 loginType=device span=659388386b080e2c
262626-01-01 00:00:00.532 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=2cb012c5bdc75561 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=216d22388570da337ea4daf0afb024ab loginType=device session_ttl=600478
262618-01-01 00:00:00.404 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=602338 span=f98902cffd632ad1 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=68c21b52e7848256be6afaf5bf72a7b4 loginType=device
262618-01-01 00:00:00.595 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=3086d3988cad0e8a4abd754605ab17ec span=98da9f895deb4e99 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=602338 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.378 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=600418 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=051c680016d2d69cdf2d78b9e0cf9385 span=a80b9e6b7be82bcc userId=611
262626-01-01 00:00:00.529 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=600418 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=b8d4d65cf470083e8b89a661bb502319 span=d581e9a9b44af313 userId=611 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.353 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=602278 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=39d183c13a2b429071716e8efdd3ea75 span=1cbd7804cb14ebad loginType=device
262618-01-01 00:00:00.535 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=602278 trace=8169aff803e13ce0726b978e54b2df9c userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=5026e01a7951338b
262626-01-01 00:00:00.361 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=600358 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=6638e69e2671925f7e6a1bf74551d587 span=40bdf02ad233b9fa path=/v1/public/user/subscribe
262626-01-01 00:00:00.514 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=600358 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=18c37ce5f2dbf3e47ca5265dd1c77545 span=174b0b9135088ac8 userId=611 loginType=device
262618-01-01 00:00:00.362 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=602218 trace=a3d077165e566b7fb7a6e591e14b6a8b path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=f3ea604d98ca6f53
262618-01-01 00:00:00.570 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=602218 trace=43b17acc6e25d7e0b0f701786453abcd span=37cab53fa0867ebe loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 path=/v1/public/user/subscribe
262626-01-01 00:00:00.367 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600298 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=126ffa53b86762e3fd295d98c1bc1c63 span=2de10d8e5abd5b08
262626-01-01 00:00:00.516 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600298 trace=0651fed479bb98153008e3bf8cab58d1 span=294bd05c890d2023 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.356 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=602158 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=18cea7939b7b521b42062d3f67309d22 span=b7e544456d676e1a loginType=device
262618-01-01 00:00:00.538 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device session_ttl=602158 trace=518a5cb6d8ff4a0260d94b9cdf2681b8 span=d7606313d9c09c70 userId=599
262626-01-01 00:00:00.362 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=13b3381fc5d0a68ac49bc6ad1c6870ee span=71e6c0f39fedf0b8 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600238 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.508 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device trace=1e63cbc1bfe0028e5fa80b8a0e40d3c8 path=/v1/public/user/subscribe session_ttl=600238 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=db3513380b6a38fb userId=611
262618-01-01 00:00:00.452 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=602098 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=f945bc757a48ca5c33f04fd89b0121ee span=6a36042d122200cb path=/v1/public/user/subscribe
262618-01-01 00:00:00.647 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c2300fa6ab79ff31ab41e47af4fbdeb8 span=aa2e5e5af0136e64 session_ttl=602098
262626-01-01 00:00:00.397 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=600178 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 loginType=device path=/v1/public/user/subscribe trace=9065ebc0baf8385a7ac1b49ca13f6edb span=5bb7d6ef1ab9a96c
262626-01-01 00:00:00.546 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=d76fb593f0918c8ad4e305b168b8d766 loginType=device session_ttl=600178 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=497035afb4ab60fc userId=611
262618-01-01 00:00:00.340 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=602038 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=251f0e41c67a65bd652a5fdd4cb85d78 span=c31951ad2df52ff7 userId=599 loginType=device
262618-01-01 00:00:00.529 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=602038 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=f4e9662d02b544a5 loginType=device trace=02887feb69ec227dc5c9892deba06660
262626-01-01 00:00:00.375 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=600118 trace=572a5d79e65f0e45929e19969f05ddfb span=c9243c118dfe64ca path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.524 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe userId=611 session_ttl=600118 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=803cda2ffd2ed460a0b50f638c26f067 span=e2679747ebc4141f
262618-01-01 00:00:00.363 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=601978 span=d923debff2a44c50 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=8a31fa044595f143becec2488c2fe899
262618-01-01 00:00:00.558 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a session_ttl=601978 trace=51488d2edce40061eb4c147aa217c887 span=b3875276456a5ee6
262626-01-01 00:00:00.382 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=600058 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 loginType=device trace=31fe9feebaffef2642fcebfdd710ed45 span=83f17374b2cfaaca
262626-01-01 00:00:00.530 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=e2ecc3261556dd58 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=600058 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=f071f3a6cd4c7a6823393045a177cabc
262618-01-01 00:00:00.353 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=f1fe597482ced34d9516ec1e61d3a824 span=b1a941dce1c046ab userId=599 session_ttl=601918 loginType=device
262618-01-01 00:00:00.541 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=c3d44e57a5b37cc1de76130f35ff4b44 span=c25bef957211dbc5 session_ttl=601918 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.374 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=46b2f4dcd50159ed2d275c077842db85 userId=611 loginType=device session_ttl=599998 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=717e5edb9fb35c35
262626-01-01 00:00:00.521 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=599998 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=bbde50903af83a512a612fa14d59d279 userId=611 path=/v1/public/user/subscribe span=72c2a72c204c8312
262618-01-01 00:00:00.340 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=601858 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=5ac5cf1ff2abd441d298be027b2b1d49 span=02f08fba364c9377
262618-01-01 00:00:00.559 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=601858 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=ee22b78d7ad23855138b9ae9dd73d97d span=94817c3e3e5fde53
262626-01-01 00:00:00.373 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=3c4d33257315c3af719c98e8a447c913 userId=611 path=/v1/public/user/subscribe session_ttl=599938 span=4ef44913265e76e5
262626-01-01 00:00:00.522 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=599938 trace=57170776d2bced4844eeb17743e82cfb loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=9bf38e18f0ce1a33
262618-01-01 00:00:00.376 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=8b4cec0ad63e7036 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=601798 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=76b7e9ac20e868a537a264e50b8455d1
262618-01-01 00:00:00.568 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=a5f9fab33bbef7cd2d2bcd7c5600ac5d span=8729b9d3ad2dc9a5 loginType=device path=/v1/public/user/subscribe session_ttl=601798
262626-01-01 00:00:00.366 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=599878 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=1c0fa92d5c97ae7ade95a5f9284a8230 span=fc3a36140bb49cc9
262626-01-01 00:00:00.509 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=599878 span=05e284039be862df loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=48108d954ecb5c24c60f1285800c724a
262618-01-01 00:00:00.487 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=601738 trace=ec116673b9152a318c031a3127267486 span=4d2f8483c88360ed sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.691 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=963d12a3d1754405 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=601738 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=183f11ddedecf119266662744a80e8e0
262626-01-01 00:00:00.370 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599818 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=44fa086e2af9b054cde634310d893807 userId=611 loginType=device path=/v1/public/user/subscribe span=697e1dd0ae9405e3
262626-01-01 00:00:00.517 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device trace=156a050c5de3458020ef455ee0829fd5 span=e69f9670ddbaac1e path=/v1/public/user/subscribe session_ttl=599818 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.420 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe trace=a7b9abc5438b4e49fbf1ec5a06ff4431 loginType=device session_ttl=601678 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=6ec52b62ed01b362
262618-01-01 00:00:00.617 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=601678 trace=d1903bbcc9046f50bc2593626a5ab293 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=ec396837ddc3518a userId=599
262626-01-01 00:00:00.349 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=599758 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=e7f351a5edc19bed7fd8a1af6d70cf15 loginType=device span=abbcae78447de444 userId=611
262626-01-01 00:00:00.488 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=599758 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=32968b6375e1a38e940b6252cb74e19e loginType=device span=f57d91bb73730c79 userId=611
262618-01-01 00:00:00.378 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=601618 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=01952f22975b981bdef737f22338eb0e span=fbdcfd4a1263bb77 userId=599 loginType=device
262618-01-01 00:00:00.567 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=5c2452377e6513f07db7deb52b192039 userId=599 path=/v1/public/user/subscribe session_ttl=601618 span=bdf4344b4fe0bfc3 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.368 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=14aee2602a4c85bfe323453037256e6b span=682c21e96569e036 userId=611 session_ttl=599698 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.511 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=9ba46ad26e471807aa637f46780fd3f9 loginType=device path=/v1/public/user/subscribe session_ttl=599698 span=7857caa4385f4f47
262618-01-01 00:00:00.399 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=25792f00da94ce40 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=6dbe4e861f646869c34a12c4cc069086 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=601558
262618-01-01 00:00:00.593 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=601558 trace=7f6a4d464493f1ee48743d9e708265b2 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=9592f77f189b8e4b
262626-01-01 00:00:00.337 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=7f275143e97bfd45993e3c07282bb871 span=c73ba2103c55e19f path=/v1/public/user/subscribe userId=611 loginType=device session_ttl=599638 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.478 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=599638 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=fa72552b3ebac1eb1e8dfaa20340e7ce span=ec7d49bed34893aa userId=611
262618-01-01 00:00:00.513 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c954b738f7c90ef66530dd44157dbd8e userId=599 session_ttl=601498 span=40c672507541f516
262618-01-01 00:00:00.736 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=7edbdbc9c1adbbfa path=/v1/public/user/subscribe session_ttl=601497 trace=5669b7bab705717e5b28d1e29de53924
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=599578 trace=022e2809f7e19d4d4e54fa859c3c5889 span=49f768e1864ccb99 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.525 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599578 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=fb4331cff44990a063cdc71d1fe95d1a span=dc0258ace78a0fce userId=611 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.640 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=468cc82e0cf0dc37 loginType=device path=/v1/public/user/subscribe session_ttl=601438 trace=a228e05d3f50ef4776efbc6e6234299a
262618-01-01 00:00:00.828 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device trace=fd2c4f777af34f9cc17038ed8c380247 span=640205736b0b94d4 path=/v1/public/user/subscribe session_ttl=601437 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.374 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599518 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=31219547f3914dfe64216db06afc26eb span=764400a6552e1b84 userId=611 loginType=device path=/v1/public/user/subscribe
262626-01-01 00:00:00.517 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=bef23adb6bdf1858b45d3a81eabf4bba userId=611 session_ttl=599518 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=b40f9ca63f3340fa
262618-01-01 00:00:00.408 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=601378 span=eaa061df5a5d325eloginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=864233d7c5fed88fd475a5f17853c3f9 userId=599 path=/v1/public/user/subscribe
262618-01-01 00:00:00.593 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=5e7f46c72fe0b33baa6f1554907d7dac session_ttl=601378 span=249390ce8399eb50 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.351 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599458 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=a41f2cd3e2a09bb9 loginType=device path=/v1/public/user/subscribe trace=243ab651963daf827b267209358db422 userId=611
262626-01-01 00:00:00.496 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=f07cb4b0bc84efebff4b6cecf76c69d3 loginType=device session_ttl=599458 span=bdc32cecfbf89399
262618-01-01 00:00:00.420 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=c821b094cdef36e8 loginType=device path=/v1/public/user/subscribe session_ttl=601318 trace=6abef8fa1b1f8844421d05193fc7d3a7
262618-01-01 00:00:00.604 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=601318 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=8bac3c5bcdb5a1ae124c6a29ef8a62ca span=0ddfdfcdb14ef876
262626-01-01 00:00:00.367 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe span=038c94e679afc1e9 loginType=device session_ttl=599398 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=76056dec0f1e4c8d6f0fd6407719797b userId=611
262626-01-01 00:00:00.517 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=7ae3748f9b07265564af94d428983b88 span=50f4365b4b016761 loginType=device path=/v1/public/user/subscribe session_ttl=599398
262618-01-01 00:00:00.339 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=40e04a93f147fc5ba37f624b008758bf span=4b20e349420e83b0 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 loginType=device session_ttl=601258
262618-01-01 00:00:00.517 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe userId=599 session_ttl=601258 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c3179ddf9ba04293586516cdd90678e9 span=659d0b878cb6bc9b loginType=device
262626-01-01 00:00:00.398 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=beff8e81aef5d448894c9abd2eafdf9e userId=611 session_ttl=599338 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=165b8e8e08669152
262626-01-01 00:00:00.548 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=599338 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=ec88a2a58251f9f28e0ec1103d06c307 span=017c2774468a3ea7 userId=611 loginType=device
262618-01-01 00:00:00.353 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=601198 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=4022127b1ccdb28f path=/v1/public/user/subscribe trace=69ac9a1511750e4962b8ef15871cfbad
262618-01-01 00:00:00.534 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=10533edb940a59b1 userId=599 path=/v1/public/user/subscribe session_ttl=601198 trace=06c3d645502dfff78feed3064ed6738e
262626-01-01 00:00:00.391 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=599278 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=ba78887c8758344637b322dac684d925 path=/v1/public/user/subscribe span=692015c48e2b122c
262626-01-01 00:00:00.544 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=599278 trace=49a8b9e849c01da6ba1c3767e5c4f3a8 span=a73fa4cabb9230e8 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.439 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=601138 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=54ce0a480492180001d64b0218544883 path=/v1/public/user/subscribe span=513e8f9ccf5eee29
262618-01-01 00:00:00.622 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=610f344e50c04abbef0375f55b0444d5 loginType=device session_ttl=601138 span=e895539dea298c2b userId=599 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=5dac85c0ecc8f6c0a0671505ff7f90b3 span=c626c143eb4487f3 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=599218 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.527 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=599218 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=3c664f7baa6a769b loginType=device trace=b228697245050162c0aafa5864fd2cb0
262618-01-01 00:00:00.358 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=601078 trace=9903124bf648ec9ed06d2b63ede351d7 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=067ee0c5b26fda71
262618-01-01 00:00:00.560 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=601078 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=0962657e0b7e7cd2ecd2a8e11b07f45a span=2d15075ed587ab97 userId=599
262626-01-01 00:00:00.393 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=599158 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=ff3f76959adb01a3 userId=611 path=/v1/public/user/subscribe trace=c39450627d87607ab5a755427b0413c3
262626-01-01 00:00:00.546 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=599158 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=45f019ae0fcb7295330d223f5eb26738 span=85fc49a7a500d1ca loginType=device
262618-01-01 00:00:00.364 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=f98b71c6d5c3d26f path=/v1/public/user/subscribe trace=428c0d266e369887876d1f200c97f092 userId=599 loginType=device session_ttl=601018
262618-01-01 00:00:00.682 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=6d8dee90b99f1e16e269d0f639bc310e span=46b9ba71c5f53601 userId=599 loginType=device session_ttl=601018
262626-01-01 00:00:00.353 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=599098 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 loginType=device trace=c811306e655d0559516344ae770309d0 span=cdfdc16e9cdde011
262626-01-01 00:00:00.500 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=599098 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=1f6a19c1a8e0db5934005e3ed437cb63 span=30c22bd2f2113289
262618-01-01 00:00:00.348 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=600958 trace=7d40617279f81d9a076eadcece7fca43 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=d83b98fed8df5fc1
262618-01-01 00:00:00.562 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=c0d05c29a08f55fe7149a05543910c32 span=5a90193c76b0b868 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 session_ttl=600958
262626-01-01 00:00:00.375 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=599038 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=394a86b0391192c3 path=/v1/public/user/subscribe trace=ed6b04d558b5058d941f3ed7d7e103b5
262626-01-01 00:00:00.520 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=599038 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=9377f4c6ed4383cb6d32b2b525bfcf01 span=69134611c974d75c
262618-01-01 00:00:00.363 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=50a3698c450b50da0c5c4a3123500000 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=ddb6f30c6f2a5c24 loginType=device path=/v1/public/user/subscribe session_ttl=600898
262618-01-01 00:00:00.563 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe loginType=device session_ttl=600898 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=6d9cf8606f44a0af5adeda46c244c383 span=bd53657a0b5e1f94
262626-01-01 00:00:00.410 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=598978 span=bacd5f6f475a8ba7 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=831db5ece16d0d58ae05604bcbdf26c4
262626-01-01 00:00:00.557 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=25379720f143896b loginType=device path=/v1/public/user/subscribe session_ttl=598978 trace=3f00bc97d14d680ca7676e3f129d0116 userId=611 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.340 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=adbc84e2d11d4f85826e12ade4fb088c span=02f06f8fd65f15df session_ttl=600838 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.524 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=600838 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a path=/v1/public/user/subscribe trace=bfeaccee1b40e467d1e68009c67419fd span=d569baf8e709def4
262626-01-01 00:00:00.361 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=e9d5bb5913dc5ae1 userId=611 path=/v1/public/user/subscribe session_ttl=598918 trace=7b387f030f01a3c6d367dec04ecf9f89 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.508 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598918 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=0795e0f81a9857ae68ea7d39990992ae span=ee49a5be5d1fcc53 userId=611 path=/v1/public/user/subscribe
262618-01-01 00:00:00.356 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=600778 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=1990b2072d31a6f45a198d448b116a46 userId=599 span=38904622ae1f1ee2
262618-01-01 00:00:00.540 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=9f1c140c1aebdc08d24b1bde53928538 userId=599 session_ttl=600778 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=0574ac6edbd8c874
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=980a552edb32a8bd userId=611 session_ttl=598858 trace=6bfd7a24f18ecefe5d4c255295f8460d
262626-01-01 00:00:00.527 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598858 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=80c6e2db15afc5bfc95dfec75d0173f6 span=1c7f565b66f8b434 userId=611 path=/v1/public/user/subscribe
262618-01-01 00:00:00.348 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=3fb8425fc142959edcff809266426b78 session_ttl=600718 span=b6bbda8dbecdaae0 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.532 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=600718 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c1713f7202414a3237e030fc3f76ccea span=913f043962560c43 loginType=device
262626-01-01 00:00:00.365 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=59e9add1cb715fae userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 loginType=device session_ttl=598798 trace=877727dc7447f9983df24de7ffb05234
262626-01-01 00:00:00.511 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598798 trace=2885c31c3f99d4dc1f44aaea9379da63 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=8804d0b95249f2c1
262618-01-01 00:00:00.356 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=600658 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=70f6bccf7ea3074dac8223dafe174b5b span=aaf3ce43acde2bce
262618-01-01 00:00:00.538 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=282cdfdfb53f1b5e userId=599 trace=a1cee29a2756715bcd376571bbc79d54 loginType=device path=/v1/public/user/subscribe session_ttl=600658 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.398 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=8afc73f2f336fa08eaf21d87924df10c span=148be0ba0b404ef5 session_ttl=598738
262626-01-01 00:00:00.548 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 session_ttl=598738 trace=5f691b9352ff6a57fe7e9e8cbc19c117 span=f191a574f022cf77
262618-01-01 00:00:00.410 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=600598 span=844b8225b34113a3 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=a6c697eddc69bae58e35d508b9b8139d userId=599
262618-01-01 00:00:00.650 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=600598 span=4e1779265910d459userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=9c915ee62cee5581a3ab8dae121745a0
262626-01-01 00:00:00.373 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=aecc876fac0baecf session_ttl=598678 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=b4770dea302ac139b222ba696dda9bbf userId=611 loginType=device path=/v1/public/user/subscribe
262626-01-01 00:00:00.518 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=598678 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=0df40235d2e868333d2706d114e62ec5 userId=611 loginType=device span=4f2595026b5b0bd2
262618-01-01 00:00:00.355 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=600538 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=04dd40d5fda3bda256f41c876456b00e userId=599 path=/v1/public/user/subscribe span=6abce42395bdd0c2
262618-01-01 00:00:00.530 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=648d0ab5c9b8aa42a377b552e61d764b span=eb569126c8d1c6be session_ttl=600538
262626-01-01 00:00:00.376 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=8b7a86b22204b0d26334c5aa3f6373e0 span=6d0ab5f9759598e3 session_ttl=598618
262626-01-01 00:00:00.530 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=c9b6434d93193fe340fa55713e280c08 span=f460e300079ca986 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=598618
262618-01-01 00:00:00.363 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device userId=599 path=/v1/public/user/subscribe session_ttl=600478 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=41ec7083ff2328e3ec905007bb4b7a76 span=790cde46b585611f
262618-01-01 00:00:00.538 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=420e1f1d1bfacf3f6d223a9f1436c083 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=d4448c75e6ff8342 session_ttl=600478
262626-01-01 00:00:00.376 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=06abcd57251b71fd userId=611 trace=05d1b7ec2a6ef24a083b2ac211e1dce8 loginType=device path=/v1/public/user/subscribe session_ttl=598558 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.520 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=9fd8eb58b37f3c324662e2ac9e0851a9 span=762c7925fa058cef loginType=device path=/v1/public/user/subscribe session_ttl=598558 userId=611
262622-01-01 00:00:00.102 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=117581474420ce47 userId=599 session_ttl=600414 trace=4d4f7ccd86fc4fca45d4779d2f52a139 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262624-01-01 00:00:00.742 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=600411 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=acc9c6832544f4a3505868ecc53496a7 span=5eddc5624e879fc9
262626-01-01 00:00:00.357 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe span=0f9de845ed1b98cb loginType=device session_ttl=598498 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=8c550dd3b0ef7284acf45ee19a0ad7d8
262626-01-01 00:00:00.498 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 loginType=device path=/v1/public/user/subscribe session_ttl=598498 trace=40686e151249142e95838ff2ae0b9082 span=52eb952323d5a218
262618-01-01 00:00:00.352 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=600358 userId=599 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=4d6a4db04cf9b44c1e3235724a9c0049 span=d741448c62adb491
262618-01-01 00:00:00.540 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=b12c32f4a8bacbd7 userId=599 loginType=device trace=4d0647812d79daadb6c82ff5804c90cb path=/v1/public/user/subscribe session_ttl=600358
262626-01-01 00:00:00.388 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device span=9c4b93b1784f66fc path=/v1/public/user/subscribe session_ttl=598438 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=c4ed7a8d873b7f3fa30802343728b7dc
262626-01-01 00:00:00.533 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe trace=e97a541e0cc62b9b03b402af6821b07a span=1042bd142709ed0f loginType=device session_ttl=598438 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.355 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=f5a96fe583833cd7e77be774bb60543a span=9fa71418fac60972 userId=599 path=/v1/public/user/subscribe loginType=device session_ttl=600298
262618-01-01 00:00:00.562 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=600298 trace=068776b22d0d2ef71a9f37cb8043ea14 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=320fb52032b4c341
262626-01-01 00:00:00.396 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 session_ttl=598378 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=6748729fbfc508f4f3bee48db5d6233a loginType=device path=/v1/public/user/subscribe span=d3176df961f6ea79
262626-01-01 00:00:00.549 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=598378 trace=00589cd5a115d0911823854defad9ebe span=78c3a2e0c097cbc8 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.322 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=600238 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=3903515b481e058a path=/v1/public/user/subscribe trace=c633b90e5aba45edb39455f0ac2efc36
262618-01-01 00:00:00.515 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=30b8dc9a64e47c2c userId=599 loginType=device session_ttl=600238 trace=cc87ea58ef347f983641434acbdf543e path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.382 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598318 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=2033bd7574918b17 userId=611 path=/v1/public/user/subscribe trace=ad9c5d636e1d1a99caf1b1c8f91f59f9
262626-01-01 00:00:00.532 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=598318 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=5d1cf065eb816ab969269e006308476d span=82ed75a7ce8db7f9 loginType=device
262618-01-01 00:00:00.287 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=600178 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=0910093c49182fb4 userId=599 trace=f17708d4ab2b32c5307bb594a23d31e4
262618-01-01 00:00:00.481 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=600178 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device path=/v1/public/user/subscribe trace=407f4b05af033ae5f3b1b111a861a171 span=b6cc55b8bebd8abb userId=599
262626-01-01 00:00:00.370 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=598258 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=c35fcf15f6db6c5718671a659840541d span=fbe7d5932ef4d700 userId=611
262626-01-01 00:00:00.522 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=598258 trace=6bcb76f9ee8be6ef6cf85dc846c86058 span=a881110a3de0e040 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.294 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=600118 trace=b3bd134f6d3111677cc12514b425f16d path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=3e90ca2a7b69f495
262618-01-01 00:00:00.488 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=600118 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=2bb014c4786f01e7cddf8b7fee7a5c4b span=b5af81df0024caa5
262626-01-01 00:00:00.361 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe trace=a4401123aeb7275673c598806dfce0f3 span=ae27fd213e16db87 session_ttl=598198 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.514 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=caf6f7e4512902a517d8ad1d9564362f session_ttl=598198 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=dfd1797288c943a0 userId=611 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.286 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=600058 trace=5b50c3093ab9a41c75dae35a24039ed0 span=ff25440fd1ac669a loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.488 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=600058 span=43a0f57860433e5cuserId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=31c969546e1218be81fd7cd14c260fee
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=b79b3736a9beb7fbdb4acee163991067 span=f4ef405d112ae8ca loginType=device session_ttl=598138 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611
262626-01-01 00:00:00.523 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=598138 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=77df358f95a7ea4b4279184824cf6960 span=2836a2af08f41130 loginType=device
262618-01-01 00:00:00.321 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=b36223e271c75ab8 path=/v1/public/user/subscribe trace=f9e3150ecc6cc17181f3ee78e3188fc4 userId=599 loginType=device session_ttl=599998
262618-01-01 00:00:00.507 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe span=be3cd07c0aa68c13 userId=599 loginType=device session_ttl=599998 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c5279907ee1f4bec13ac0b9681b4a985
262626-01-01 00:00:00.392 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=73986b0e28eae9ef loginType=device trace=3577b0cee20fda1cd17861667e52b534 userId=611 path=/v1/public/user/subscribe session_ttl=598078
262626-01-01 00:00:00.544 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=598078 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=faaac56a8ba206a5379031b336cc531d span=3147c232dd51af38
262616-01-01 00:00:00.017 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=969f09a529c0c17615daddac1b179b39 span=132e346c811fbc3b userId=599 loginType=device path=/v1/public/user/info session_ttl=599940 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262616-01-01 00:00:00.021 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=d00df9447c4ddbd8 userId=599 loginType=device path=/v1/public/subscribe/list session_ttl=599940 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=eff86fedfce046208a3f4d42e2a0641c
262616-01-01 00:00:00.025 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=b8d691af523fbc2c userId=599 loginType=device path=/v1/public/payment/methods trace=3de348e1ab5d032a539102ddd40ff551 session_ttl=599940
262616-01-01 00:00:00.032 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=599940 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=9c54cf9291521b55 path=/v1/public/user/subscribe trace=462d21d45bf609dce789a4401041295b
262618-01-01 00:00:00.347 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=0662ff0d5aaecce30013243bd499540d userId=599 span=303e5a47a9f67a29 loginType=device path=/v1/public/user/subscribe session_ttl=599938 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.539 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=7521d036e03468c552670618c888bc8d span=e0e1de06485cc521 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=599938
262626-01-01 00:00:00.370 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=cd0f2203981a5c99 loginType=device session_ttl=598018 trace=f052eb092f193e18e523d8e32ae071d5
262626-01-01 00:00:00.521 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=196671a893cba0b9 session_ttl=598018 trace=0d44cbe8f885999886ce6e7f0140a0bd
262626-01-01 00:00:00.579 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device trace=102d88f1f6dc2c99bd18cf0fbfb07b17 userId=599 path=/v1/public/user/info session_ttl=599930 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=ebe0bf97009df8e2
262626-01-01 00:00:00.601 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599930 span=9b676ff2d0fbdc0auserId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=c64ad9b6234e0ab1681006ce90716c22 loginType=device path=/v1/public/user/subscribe
262626-01-01 00:00:00.618 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=eb5c8b81aa935a3db5eefbd78772549d span=8ff9b8bd30391202 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 path=/v1/public/subscribe/list session_ttl=599930
262626-01-01 00:00:00.618 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/payment/methods sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=1fa7f679e4958d082cfeb6985c9f47ed userId=599 session_ttl=599930 span=d0a5718b0cc8d348
262618-01-01 00:00:00.281 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=e76284f0cf07c5df2d35b546d34f933d span=7cc933fc632c2312 userId=599 path=/v1/public/user/subscribe session_ttl=599878
262618-01-01 00:00:00.479 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=dc23b9f686cf34cb userId=599 loginType=device trace=b868f48194ae9241c896dc5693586291 path=/v1/public/user/subscribe session_ttl=599878
262626-01-01 00:00:00.386 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=b90b8885f420cc900cd9d2f6c5fec7b4 span=874f816a3f87b14c path=/v1/public/user/subscribe userId=611 loginType=device session_ttl=597958
262626-01-01 00:00:00.539 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=eeb87e152985d284977843bba8005b30 span=6005298d50b33059 loginType=device session_ttl=597958
262618-01-01 00:00:00.421 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=fb68f52e9a808d66 path=/v1/public/user/subscribe session_ttl=599818 trace=5e6e777fe3cd91e28f622132d26533db
262618-01-01 00:00:00.668 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=599818 trace=478a3bd765f4fe2fa8d4dcc2c75381cc path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=baa83739e50f478c
262626-01-01 00:00:00.364 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=597898 trace=8365c5977c99529865150355fe9dbdcb span=b4b245f6e4f051b2 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.518 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=da0a839484913cac89572d5c1c0283a9 span=f9c878cb4e2b41ea userId=611 path=/v1/public/user/subscribe session_ttl=597898
262618-01-01 00:00:00.292 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 loginType=device session_ttl=599758 trace=267d0776d7c43e45bbbe89ad5a15b0e7 span=41b9b55ac7c59358
262618-01-01 00:00:00.487 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=950df849c8c8aa8f43ebae4e5f6d1213 span=7b54e9d44803a369 userId=599 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device session_ttl=599758
262626-01-01 00:00:00.379 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=597838 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=8acc0ac370dcc9f2812ffb436f33cbea path=/v1/public/user/subscribe span=9e4490670c396f7c
262626-01-01 00:00:00.531 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=597838 trace=93a1c895ea0a47bae03c452859e2d536 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=f9a1056b2cd8a9a9 userId=611
262618-01-01 00:00:00.326 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=335da02ffb7febc8df8638d518288d2c userId=599 loginType=device session_ttl=599698 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=c22a96334e31a4a8
262618-01-01 00:00:00.515 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=26f95d51251345681a48d8fbcc1423f9 userId=599 loginType=device path=/v1/public/user/subscribe span=b28af7f863475527 session_ttl=599698 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.362 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=00d323fe6ae0d6a6 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=597778 trace=45d4109c143d39835428934a68fc2d4b
262626-01-01 00:00:00.509 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=597778 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=c99ebb7b43d15c37a1d336b3f5b90d58 span=f828d0b992156988 userId=611 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.318 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=d14c07133ebf8d85996125052f5d493e session_ttl=599638 span=9421246ec6eafce0 userId=599 loginType=device
262618-01-01 00:00:00.512 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=f6339cbc5cade57bad1c3f0ef5426471 span=656a21d3a990ccf8 session_ttl=599638 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.379 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=597718 span=3c1893d3d6aba413 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=207d63941b423adb2bd775f99c21de19
262626-01-01 00:00:00.528 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=597718 trace=ca70c2037dc749520c1a17d415b5ddb2 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=6c6a638ad1d94707
262618-01-01 00:00:00.326 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=599578 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=67843b2e4a252f5dd26d80e918e24807 span=3b8d977489099e57
262618-01-01 00:00:00.512 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=c2a1a847f1082cb1 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=b0bd00f09b3644dcb3b6061d6114aa1e loginType=device path=/v1/public/user/subscribe session_ttl=599578
262626-01-01 00:00:00.381 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=597658 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=d0a4e181eaa62600150139e1149fef2e span=4351c2621ce079c2 userId=611
262626-01-01 00:00:00.529 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=e68e54b65e7d69f984bbb3e700b86b58 span=19c589252f275fc4 userId=611 session_ttl=597658
262618-01-01 00:00:00.278 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=599518 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=5a817fdbc7d54b30 userId=599 path=/v1/public/user/subscribe trace=acba070a7a1233d3b50520c6b5e28de1
262618-01-01 00:00:00.458 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=2dd9f4d9f2402224 path=/v1/public/user/subscribe trace=aebfa92f95e169c0299849f7d8410221 userId=599 loginType=device session_ttl=599518
262626-01-01 00:00:00.357 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=cde8a241089d187a loginType=device userId=611 path=/v1/public/user/subscribe session_ttl=597598 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=202d4c109b853775a7bd288eadbcceb5
262626-01-01 00:00:00.501 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=0a75957f62568e41 userId=611 session_ttl=597598 trace=b727c8888d4de9edd408a6c63f3e9469 loginType=device
262618-01-01 00:00:00.284 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=2e3b667384a06ceb userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device path=/v1/public/user/subscribe session_ttl=599458 trace=66e8b0e832bd4fcdf2b894ae924ac78c
262618-01-01 00:00:00.480 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=f381d50729759a88 userId=599 trace=0e1b4fdb353be575f46070d7dd8ca4f5 loginType=device path=/v1/public/user/subscribe session_ttl=599458
262626-01-01 00:00:00.380 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=597538 trace=9d8e80e9badcbb080c0587a2163b34b2 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=d43a76b5604a5e63
262626-01-01 00:00:00.531 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=597538 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=5f24837279ede4a2989fcb4a929cd9aa span=c2d42fd4e619cc34 loginType=device
262618-01-01 00:00:00.277 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device session_ttl=599398 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=a8bedf34bcbd95e6 path=/v1/public/user/subscribe trace=f8bdebf5f9ad528e43d8c8161a707dcb
262618-01-01 00:00:00.459 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 trace=a32439d94d5735c97e44d66db767d246 span=31e8c3f8840e0335 loginType=device path=/v1/public/user/subscribe session_ttl=599398 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 session_ttl=597478 trace=0dcd02d420192610daa7031558d26afe span=c8f63734b3403bde
262626-01-01 00:00:00.520 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=597478 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 trace=81d0090dcc30b10c8579641491932117 span=cc629a1df7dc2fab
262618-01-01 00:00:00.296 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599338 span=f93768d5042ffa54userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=1b0f01e518d45d0f584deab644732e48 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.490 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=599338 span=61b0d063e45c03bc loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=2c5d7f24aca199d223926a07d0ccb952
262626-01-01 00:00:00.389 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=c65304532cc1ce0ef3c0bf96560b8b20 span=93eb50d20c777559 userId=611 loginType=device session_ttl=597418 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.536 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=b61bcbced935cf2317f289454bc56020 span=71060b030f3a371d loginType=device path=/v1/public/user/subscribe userId=611 session_ttl=597418
262618-01-01 00:00:00.282 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599278 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=549b950faf330793fd012fdcc0aed472 span=9ee4c5310c9ceeef userId=599 loginType=device path=/v1/public/user/subscribe
262618-01-01 00:00:00.486 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=ca55bfcf510804f1cff7c0b875b31e35 span=8ea6fd90800f3656 loginType=device path=/v1/public/user/subscribe session_ttl=599278
262626-01-01 00:00:00.345 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=597358 trace=5ff45a30640e171817f973b78a19a84e sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=8d62347ff57e7e82
262626-01-01 00:00:00.492 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=8847bb8020f4bd519b86e0dbebbced9e span=3a4cd4cdde65f485 session_ttl=597358 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611
262618-01-01 00:00:00.282 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device trace=7f421786dde6bb4397b8d930c763d73d userId=599 path=/v1/public/user/subscribe session_ttl=599218 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=3cfcc144b0690eee
262618-01-01 00:00:00.478 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=d9d4654e309c92ffc30be9a289df89b0 span=433c412ff0017a72 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a session_ttl=599218
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=7624841abc2badf18e02b9d102c52bac loginType=device span=f9699e8889cd2ffd userId=611 path=/v1/public/user/subscribe session_ttl=597298 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.525 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=597298 trace=8d60e2c212c1c8d151fced6e3e2ba8a2 userId=611 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=f2ab81f5a29ad66a
262618-01-01 00:00:00.283 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=599158 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=38c1ebb0724284e5 userId=599 loginType=device path=/v1/public/user/subscribe trace=edf651af0bf09668947ec2806551dbe6
262618-01-01 00:00:00.485 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=599158 trace=8cf1b7c79cb3c35d25fe7b7f2163b7e6 span=ea1e265de9b5f126 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.380 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe session_ttl=597238 span=2f254b20dd0a2d33 userId=611 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=5de8d0fe93c58f6cf32db5558a488776
262626-01-01 00:00:00.526 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=597238 trace=89903b0037009b16e317bc95a0f14575 span=970d2cb43d71e31e path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262618-01-01 00:00:00.295 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=a41e214a05087990c7c9de806bd68724 span=030bc7d80cb22b04 userId=599 loginType=device session_ttl=599098 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.488 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=0f6275f412cd5faa8326cd1bc6b9e762 userId=599 path=/v1/public/user/subscribe session_ttl=599098 span=60fe106ea66b2e18 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.363 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 session_ttl=597178 span=5908e6b20cf18c34 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=cc208a3eb58e7100f299baafaa16b86c
262626-01-01 00:00:00.509 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=597178 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=e7b9e4a2239fa88db6eb891d30e202df userId=611 loginType=device path=/v1/public/user/subscribe span=56fc902932d5c3b2
262618-01-01 00:00:00.326 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=14583f55d6a9233c67ba0461d90825b5 session_ttl=599038 span=08eaa3a4a6bddda2
262618-01-01 00:00:00.505 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=a11dd9dbb6435480 userId=599 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device session_ttl=599038 trace=e12dc22427cdb488ff0c8fad8f6ffb4e
262626-01-01 00:00:00.369 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=c422f64d98249f8e6aec645e71fe8196 path=/v1/public/user/subscribe session_ttl=597118 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=8d49fd47301f7be7 userId=611 loginType=device
262626-01-01 00:00:00.517 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe trace=4e8fbd080bfdc518702091ee2dfd8c33 userId=611 session_ttl=597118 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=dd41cb8efbc10ece
262618-01-01 00:00:00.321 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=598978 span=44e41cc76b989200loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=a03144440b263a69a7413b3676ffef1f userId=599
262618-01-01 00:00:00.534 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598978 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=d56ec109f029d960 userId=599 path=/v1/public/user/subscribe trace=5a3b4309a513ab0a6685c3dc8be1797c
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=597058 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=72ea4d04232b6e549ab90d9ef5f26476 userId=611 path=/v1/public/user/subscribe span=cb007ee01325ab0c
262626-01-01 00:00:00.516 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device span=dfe2a0132cf9c832 path=/v1/public/user/subscribe session_ttl=597058 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=59e95a82f149c4033b14b7a806c6046a
262618-01-01 00:00:00.275 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 span=4137bb77ff86e61e path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=19d7f7ab04b9a30a310dd7a4fb65c259 userId=599 loginType=device session_ttl=598918
262618-01-01 00:00:00.479 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=598918 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=003a08407c18318ea8566184fac2a464 userId=599 loginType=device path=/v1/public/user/subscribe span=ea487e8c63a41148
262626-01-01 00:00:00.372 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=596998 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=186774867e897d12bfec75dc462fd2e5 path=/v1/public/user/subscribe span=13b2935d650c8553
262626-01-01 00:00:00.514 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=596998 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 userId=611 path=/v1/public/user/subscribe trace=315bcbe8ba36494c63cbbdfe1dd5f78b span=79e2abb7d268fbb1
262618-01-01 00:00:00.364 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=2215c6be13f56ee4786769a67e989698 span=de2439619bccdc1e loginType=device session_ttl=598858 userId=599 path=/v1/public/user/subscribe
262618-01-01 00:00:00.570 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=598858 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=e1dff85ad82bdc31f518002b19254c60 loginType=device span=03ed0217da496d62
262626-01-01 00:00:00.377 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device session_ttl=596938 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 path=/v1/public/user/subscribe trace=b98665eb9a6f8eb109545ed7e0290285 span=f4aea59b1b8460d0
262626-01-01 00:00:00.530 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe span=b5ad803a2c173471 session_ttl=596938 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=4d56ee65434c839b838f0fb8ea3b0c1e userId=611
262618-01-01 00:00:00.281 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598798 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=82757a39bb82a813 userId=599 path=/v1/public/user/subscribe trace=f118a47159308457ddd8b51f01da3d58
262618-01-01 00:00:00.478 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=2533086f176572a9e825b7b1e31dc938 loginType=device path=/v1/public/user/subscribe session_ttl=598798 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=4ee7dae57576ae8c userId=599
262626-01-01 00:00:00.379 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device trace=05cbc030937dca3ea74fd54e1f040d93 span=cd3840f27d67124b path=/v1/public/user/subscribe session_ttl=596878 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.527 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe trace=dc94054d97451b98ab79b18ada6b6f75 span=7c1a324285ac2866 userId=611 session_ttl=596878 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 loginType=device
262618-01-01 00:00:00.326 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=b82e72733c1662ed loginType=device trace=20fa599dd3daf7863df0ead65c080942 userId=599 path=/v1/public/user/subscribe session_ttl=598738
262618-01-01 00:00:00.510 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device trace=359487c175a3406689ab1a4fae300a3f span=9c087fb3208986cf path=/v1/public/user/subscribe session_ttl=598738 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262626-01-01 00:00:00.389 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe session_ttl=596818 loginType=device sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=0a1fe8d075292ad1e27ac769338e5746 span=95efa8899f17458e
262626-01-01 00:00:00.545 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 session_ttl=596818 trace=e00b16784d0589585df78fd37fa8ab2a span=bfde0c9da25346e8
262618-01-01 00:00:00.283 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=598678 trace=58b311132d07665df7cf453880137e25 span=91d9a0d8ca0c4c24
262618-01-01 00:00:00.476 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe session_ttl=598678 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=16d22716c182cf4dab10e391ab0a559d span=3accb68b03b55ea6
262626-01-01 00:00:00.420 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device trace=bd5d282d7a479e71b6756b24591cb820 span=18b58bd8a7acf4bf path=/v1/public/user/subscribe session_ttl=596758 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60
262626-01-01 00:00:00.570 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe session_ttl=596758 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=4ab21f511c5e0879d4ec9262c3174401 span=384d215ae023144a
262618-01-01 00:00:00.351 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=598618 trace=aca78501e05d5029ec9a3471ae78a566 span=110415ba6362a1d6 userId=599 path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a
262618-01-01 00:00:00.586 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=628fd4f4d246aa70 path=/v1/public/user/subscribe session_ttl=598618 trace=4dac9884d80324f78ee6cf6dc363e2e2
262626-01-01 00:00:00.370 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=596698 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=a8d4cb5429c6016f8982b3e1528e6c3a span=035ff5cd23925763 userId=611 loginType=device path=/v1/public/user/subscribe
262626-01-01 00:00:00.518 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device path=/v1/public/user/subscribe session_ttl=596698 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=9e46bdde4a09a064 userId=611 trace=d6bdd936634e19135d26d38f280863c0
262618-01-01 00:00:00.297 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 trace=4cd73356ecd2bc83b403c4ca1532f74f span=31797838fcf5baa4 userId=599 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a loginType=device path=/v1/public/user/subscribe session_ttl=598558
262618-01-01 00:00:00.527 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 path=/v1/public/user/subscribe session_ttl=598558 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=ab179ce5636be9ea56fe6f06c4db117c loginType=device span=fa48af433326c2a4
262626-01-01 00:00:00.386 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=596638 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=1bae4e8d9c995c08185b4c7435f9d8d5 span=566ef9240d7ad446 userId=611 loginType=device path=/v1/public/user/subscribe
262626-01-01 00:00:00.531 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 session_ttl=596638 span=3e4c721d5ba7b703 loginType=device path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=59a103212a009a98d441b9b37062b0c7
262618-01-01 00:00:00.290 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 loginType=device path=/v1/public/user/subscribe sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=d997cf8f5b05a40f session_ttl=598498 trace=93c3b403d021f3eaa96f38e236deea2a
262618-01-01 00:00:00.489 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=598498 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=bd2b5823673236b7 loginType=device path=/v1/public/user/subscribe trace=9c154e5bf9ba427090a992ef04d1b41a
262626-01-01 00:00:00.381 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 session_ttl=596578 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=9a0c9c91c0310e7853a1d9e9d4aa0d0f span=c63e95ab405a6f0e userId=611 loginType=device path=/v1/public/user/subscribe
262626-01-01 00:00:00.534 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 trace=0b64d07c40a7ec11f063769ba7dcf0a4 span=dd801ac13cc8c760 loginType=device session_ttl=596578
262618-01-01 00:00:00.318 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=599 session_ttl=598438 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a span=c2326fcd0b92e207 loginType=device path=/v1/public/user/subscribe trace=79b9130c4f2082eb2c6fa7cc31d9f632
262618-01-01 00:00:00.508 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bf918-48d2-752b-b6b4-d8af97d5567a trace=0d8c46e3a6100bc243dfaa29fd7be1e9 span=4268ca3ee43415ac path=/v1/public/user/subscribe session_ttl=598438 userId=599 loginType=device
262626-01-01 00:00:00.374 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 path=/v1/public/user/subscribe sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=813bafc6b9aacd3a userId=611 loginType=device session_ttl=596518 trace=b4642fee9e0fef388d4ed3e6f82a36e2
262626-01-01 00:00:00.521 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=611 loginType=device path=/v1/public/user/subscribe trace=39b826161c43dee5f9e0df8e13e9731b session_ttl=596518 sessionId=019bf9ee-f10b-752b-81de-d5bcffecdb60 span=98a98c633da0fa35
26268-01-01 00:00:00.521 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=604799 trace=dec35193dbfe742a610b6eca2738bb94 span=7b097282f9d8deb7 path=/v1/public/user/info sessionId=019bfa7a-c2c0-752b-ad80-de497ec7cfd0 userId=553
26268-01-01 00:00:00.530 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=553 loginType=device path=/v1/public/user/subscribe sessionId=019bfa7a-c2c0-752b-ad80-de497ec7cfd0 trace=bdcbe9e2f65e77555eb583ac531fa506 session_ttl=604799 span=20b31bd9899fbcf6
26268-01-01 00:00:00.721 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 userId=553 trace=e5b92a0084a0bf44ac976d53b548f41e span=5bda957e7944f445 loginType=device path=/v1/public/user/subscribe session_ttl=604799 sessionId=019bfa7a-c2c0-752b-ad80-de497ec7cfd0
26268-01-01 00:00:00.920 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 sessionId=019bfa7a-c2c0-752b-ad80-de497ec7cfd0 trace=ec94a52af4b9ec3289cfc57501c696dc userId=553 span=2d74145a62b6d9d9 loginType=device path=/v1/public/subscribe/node/list session_ttl=604799
262635-01-01 00:00:00.331 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=604473 trace=fc96231dc5709e4c40c60ed0636ca36c span=6d5765b511ad0c9e userId=553 path=/v1/public/user/subscribe sessionId=019bfa7a-c2c0-752b-ad80-de497ec7cfd0
262635-01-01 00:00:00.894 info [AuthMiddleware] auth ok caller=middleware/authMiddleware.go:104 loginType=device session_ttl=604472 sessionId=019bfa7a-c2c0-752b-ad80-de497ec7cfd0 trace=75947b71672031173ba9ce91cee1b38a span=603f88dd5d0efdae userId=553 path=/v1/public/user/subscribe

0
malware_sample.sh Normal file
View File

View File

@ -1,37 +0,0 @@
server {
listen 80;
server_name localhost; # 请修改为您实际的域名,如 ppanel.example.com
# ----------------------------------------------------
# 1. Nginx Status (探针/监控端点)
# ----------------------------------------------------
# 用于 nginx-exporter 采集 Nginx 自身指标 (连接数、请求数等)
location /nginx_status {
stub_status on;
access_log off;
# 允许 Docker 容器和本地访问
allow 127.0.0.1;
allow 172.16.0.0/12; # Docker bridge 默认网段
allow 192.168.0.0/16; # Docker Desktop for Mac/Windows 网段
allow 10.0.0.0/8; # 其他常见私有网段
deny all;
}
# ----------------------------------------------------
# 2. PPanel Server (后端 API)
# ----------------------------------------------------
# 将请求转发到宿主机 8080 端口 (docker-compose 中暴露的端口)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 支持 (如果需要)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}

View File

@ -0,0 +1,18 @@
package pkgaes
import (
"fmt"
"testing"
)
func TestManualDecrypt(t *testing.T) {
cipherText := "rLuw+6cV+o3+pVoMdeZ0vOqoRaRvMpUV7VNgEXY9qYFOdGPZ5eQ6KashmOI1d7B6lzbYa0ccqOGFBM2Xfon4GzF/WrYf+jyWD673UIWGiQt4QOsUBZ7k7X1wMHYXsZGaau4mv0YD/8b5raY6s/QNh5mdihXTsdsZ1PIPmpmoiYVMUcOl1WUfoSXg/iSB5aX64Rb9NvPeRFExoo22A+rPpP3n1txMOecmDBBaoCwEr5lUF5I53d/DaZxjzB0BJ9RcA0jaaecuvDG7QJ6n7kVFtWlB+OEBmtklN17a9Bh0m+9DOB9axu3FjBsOaterDa0ufJtfyW/jPvCgKZKclzNS8xhrZrDY9BUt8kIpPRTi6974q8rayvl/ISxQihOm/FiJ+x/zEr6hLekFhXvlDPcV5lyzT6wjUEldkM0u3Ldiqdv3e0eYUyqoaTjnJCjlSfkb2wKX14bn984tYK5IfU6OjLCEUSiAFSzeHtEmpfb+861sJq/EJep7TeEsUqJZNRY2KUAawUjnAtKSlX7kHjvZGFicZqlQUGcha9CPSOpwnGeZz51q5JXJo1H7CqnYGyZZZrIkB+qi4ZK0EGkO0Mm/cLun5a1sWdkgfQixQW4jKRnjrdohAlbLV4AC9tUODUzgl1Ot+7xP2+zo4SbOGQ8zE+sthtBme0NeMHjW00magCHJbpV+bnVZHr3jGpQQUMAdNAyRpQdIn3Nitv3Hun/HLU3EhT38dIBHGkx47RMN0NKkKcePqN3ImIdqsM1jR+GnK5oM2qUlra2tk06bKHMAOo8csmIMIwwh7yhI0bz+UaMDQjPf6/YdYNQaB40vAokbjxC1pEWDvpSR+QSn7NnXzLZf5iwTvkErwWolbJbJCV7YA5zq1PBNVkSY5d3lj0iVe9/oDqcnDWOFgXrbZ1+QowceJSumEXna7M5RMp7tBJUxlyTag7z1jKVBqWv7ydWNQnBT+MpzHLzXOdAuClrKwrYgeaXafqsTZvsNGA7Jtfr+eWIfifT1f/6yqiOa90ZqPv1dmpSIgOTA9NpOPvChQ1VicC9SiS/q0lD9/ZD5H9PvmFylo4DGKGmpEXrnOSy2770WCmNJmjuxf68NBcR/6mN9JBd7XnS+BjGXymybMIVZvy/dsC6zU2AKSolSNjeraP3zNl1fOQNNFDDnd+y/z4RHgbRvGg/BkYygx+sz0Fc87miccLIG2fIaJeGNSg+VFSlWlAFmhv07hZk6k3g/+J+3u1jUiCtQfAoUcE2f18OtdOCSib49e63uCH1NQIHp"
keyStr := "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx"
ivStr := "188ec64c36b50c13"
plaintext, err := Decrypt(cipherText, keyStr, ivStr)
if err != nil {
t.Fatalf("Decryption failed: %v", err)
}
fmt.Printf("\nDEC_START\n%s\nDEC_END\n", plaintext)
}

File diff suppressed because one or more lines are too long

View File

@ -161,3 +161,89 @@ func (c *Client) CreateInviteShortLink(ctx context.Context, baseURL, inviteCode,
return link.Link, nil return link.Link, nil
} }
// PeriodStats 时间段统计数据
type PeriodStats struct {
Total int `json:"total"` // 总访问量
Views []int `json:"views"` // 时间序列数据(按天/小时)
Stats StatsDetail `json:"stats"` // 详细统计
}
// StatsDetail 详细统计信息
type StatsDetail struct {
Browser []StatItem `json:"browser"` // 浏览器分布
OS []StatItem `json:"os"` // 操作系统分布
Country []StatItem `json:"country"` // 国家分布
Referrer []StatItem `json:"referrer"` // 来源分布
}
// StatItem 统计项
type StatItem struct {
Name string `json:"name"`
Value int `json:"value"`
}
// LinkStatsResponse 链接详细统计响应
type LinkStatsResponse struct {
ID string `json:"id"`
Address string `json:"address"`
Link string `json:"link"`
Target string `json:"target"`
VisitCount int `json:"visit_count"`
LastDay PeriodStats `json:"lastDay"`
LastWeek PeriodStats `json:"lastWeek"`
LastMonth PeriodStats `json:"lastMonth"`
LastYear PeriodStats `json:"lastYear"`
}
// GetLinkStats 获取链接的详细统计数据
//
// 参数:
// - ctx: 上下文
// - linkID: 链接的 UUID
//
// 返回:
// - *LinkStatsResponse: 详细统计数据
// - error: 错误信息
func (c *Client) GetLinkStats(ctx context.Context, linkID string) (*LinkStatsResponse, error) {
// 创建 HTTP 请求
url := fmt.Sprintf("%s/links/%s/stats", c.apiURL, linkID)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create request failed: %w", err)
}
// 设置请求头
httpReq.Header.Set("X-API-KEY", c.apiKey)
httpReq.Header.Set("Content-Type", "application/json")
// 发送请求
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("send request failed: %w", err)
}
defer resp.Body.Close()
// 读取响应体
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response failed: %w", err)
}
// 检查响应状态
if resp.StatusCode != http.StatusOK {
var errResp ErrorResponse
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error != "" {
return nil, fmt.Errorf("kutt api error: %s - %s", errResp.Error, errResp.Message)
}
return nil, fmt.Errorf("kutt api error: status %d, body: %s", resp.StatusCode, string(respBody))
}
// 解析响应
var stats LinkStatsResponse
if err := json.Unmarshal(respBody, &stats); err != nil {
return nil, fmt.Errorf("unmarshal response failed: %w", err)
}
return &stats, nil
}

159
pkg/loki/loki.go Normal file
View File

@ -0,0 +1,159 @@
package loki
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
// Client Loki 客户端
type Client struct {
url string
httpClient *http.Client
}
// NewClient 创建新的 Loki 客户端
// url: Loki 服务地址,例如 http://154.12.35.103:3100
func NewClient(url string) *Client {
return &Client{
url: url,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// InviteCodeStats 邀请码统计数据
type InviteCodeStats struct {
MacClicks int64 `json:"mac_clicks"` // Mac 下载点击数
WindowsClicks int64 `json:"windows_clicks"` // Windows 下载点击数
LastMonthMac int64 `json:"last_month_mac"` // 上月 Mac 下载数
LastMonthWindows int64 `json:"last_month_windows"` // 上月 Windows 下载数
}
// LokiQueryResponse Loki 查询响应结构
type LokiQueryResponse struct {
Status string `json:"status"`
Data struct {
ResultType string `json:"resultType"`
Result []struct {
Stream map[string]string `json:"stream"`
Values [][]string `json:"values"` // [[timestamp, log_line], ...]
} `json:"result"`
} `json:"data"`
}
// GetInviteCodeStats 获取指定邀请码的下载统计
// inviteCode: 邀请码
// days: 统计天数默认30天
func (c *Client) GetInviteCodeStats(ctx context.Context, inviteCode string, days int) (*InviteCodeStats, error) {
if days <= 0 {
days = 30
}
now := time.Now().UTC()
startTime := now.Add(-time.Duration(days) * 24 * time.Hour)
// 上月时间范围
lastMonthEnd := startTime
lastMonthStart := startTime.Add(-time.Duration(days) * 24 * time.Hour)
// 查询本月数据
thisMonthStats, err := c.queryPeriodStats(ctx, inviteCode, startTime, now)
if err != nil {
return nil, fmt.Errorf("查询本月数据失败: %w", err)
}
// 查询上月数据
lastMonthStats, err := c.queryPeriodStats(ctx, inviteCode, lastMonthStart, lastMonthEnd)
if err != nil {
return nil, fmt.Errorf("查询上月数据失败: %w", err)
}
return &InviteCodeStats{
MacClicks: thisMonthStats.MacClicks,
WindowsClicks: thisMonthStats.WindowsClicks,
LastMonthMac: lastMonthStats.MacClicks,
LastMonthWindows: lastMonthStats.WindowsClicks,
}, nil
}
// queryPeriodStats 查询指定时间范围的统计数据
func (c *Client) queryPeriodStats(ctx context.Context, inviteCode string, startTime, endTime time.Time) (*InviteCodeStats, error) {
// 构建 Loki 查询
query := fmt.Sprintf(`{job="nginx_access", invite_code="%s"}`, inviteCode)
apiURL := fmt.Sprintf("%s/loki/api/v1/query_range", c.url)
params := url.Values{}
params.Add("query", query)
params.Add("start", startTime.Format(time.RFC3339))
params.Add("end", endTime.Format(time.RFC3339))
params.Add("limit", "5000")
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("Loki 返回错误状态码 %d: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %w", err)
}
var lokiResp LokiQueryResponse
if err := json.Unmarshal(body, &lokiResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
// 解析日志行统计 Mac 和 Windows 下载
stats := &InviteCodeStats{}
// Nginx combined log format regex
// 格式: IP - - [time] "METHOD URI VERSION" STATUS BYTES "REFERER" "UA"
logPattern := regexp.MustCompile(`"[A-Z]+ ([^ ]+) `)
for _, result := range lokiResp.Data.Result {
for _, value := range result.Values {
if len(value) < 2 {
continue
}
logLine := value[1]
// 提取 URI
matches := logPattern.FindStringSubmatch(logLine)
if len(matches) < 2 {
continue
}
uri := strings.ToLower(matches[1])
// 统计平台下载
if strings.Contains(uri, "mac") {
stats.MacClicks++
} else if strings.Contains(uri, "windows") {
stats.WindowsClicks++
}
}
}
return stats, nil
}

View File

@ -0,0 +1,68 @@
package openinstall
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// TestChannelParameter 验证 OpenInstall 客户端是否正确传递了 channel 参数
func TestChannelParameter(t *testing.T) {
// 1. 启动 Mock Server
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 验证请求路径
if r.URL.Path == "/data/sum/growth" {
// 验证 Query 参数
query := r.URL.Query()
channel := query.Get("channel")
// 核心验证点channel 参数必须等于即使的 inviteCode
if channel == "TEST_INVITE_CODE_123" {
w.WriteHeader(http.StatusOK)
// 返回假数据
w.Write([]byte(`{
"code": 0,
"body": [
{"key": "ios", "value": 100},
{"key": "android", "value": 200}
]
}`))
return
}
// 如果 channel 不匹配,返回错误
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"code": 400, "error": "channel mismatch"}`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer mockServer.Close()
// 2. 临时修改 apiBaseURL 指向 Mock Server
originalBaseURL := apiBaseURL
apiBaseURL = mockServer.URL
defer func() { apiBaseURL = originalBaseURL }()
// 3. 初始化客户端
client := NewClient("test-api-key")
// 4. 调用接口 (传入测试用的邀请码)
ctx := context.Background()
stats, err := client.GetPlatformDownloads(ctx, "TEST_INVITE_CODE_123")
// 5. 验证结果
assert.NoError(t, err)
assert.NotNil(t, stats)
// 验证数据正确解析 (iOS=100, Android=200, Total=300)
assert.Equal(t, int64(100), stats.IOS, "iOS count should match mock data")
assert.Equal(t, int64(200), stats.Android, "Android count should match mock data")
assert.Equal(t, int64(300), stats.Total, "Total count should match sum of mock data")
t.Logf("Success! Channel parameter 'TEST_INVITE_CODE_123' was correctly sent to server.")
}

View File

@ -0,0 +1,57 @@
package openinstall
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestClient_GetPlatformDownloads_WithChannel(t *testing.T) {
// Mock Server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify URL parameters
assert.Equal(t, "GET", r.Method)
assert.Equal(t, "/data/sum/growth", r.URL.Path)
assert.Equal(t, "test-api-key", r.URL.Query().Get("apiKey"))
assert.Equal(t, "test-channel", r.URL.Query().Get("channel")) // Verify channel is passed
assert.Equal(t, "total", r.URL.Query().Get("sumBy"))
assert.Equal(t, "0", r.URL.Query().Get("excludeDuplication"))
// Return mock response
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{
"code": 0,
"body": [
{"key": "ios", "value": 10},
{"key": "android", "value": 20}
]
}`))
}))
defer server.Close()
// Redirect base URL to mock server (This requires modifying the constant in real code,
// but for this test script we can just verify the logic or make the URL configurable.
// Since apiBaseURL is a constant, we cannot change it.
// However, this test demonstrates the logic we implemented.
// For actual running, we might need to inject the URL or make it a variable.)
// NOTE: Since apiBaseURL is constant in standard Go we can't patch it easily without unsafe or changing code.
// But `getDeviceDistribution` constructs the URL using `apiBaseURL`.
// For the sake of this example, we assume we can test the parameter construction logic
// or we would need to refactor `apiBaseURL` to be a field in `Client`.
// Since I cannot change the constant easily to point to localhost in the compiled package
// without refactoring, I will provide a test that *would* work if we refactored,
// OR I can make the test just run against the real API but that requires a key.
// Plan B: Create a test that instantiates the client and checks the URL construction if we extracted that method,
// but we didn't.
// Let's refactor Client to allow base URL injection for testing?
// Or just provide a shell script for the user to run against real env provided they have keys.
// The user asked for a "Test Script", commonly meaning a shell script to run the API.
t.Log("This is a structural test example. To fully unit test HTTP requests with constants, refactoring is recommended.")
}

View File

@ -0,0 +1,290 @@
package openinstall
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
var (
// OpenInstall 数据接口基础 URL
apiBaseURL = "https://data.openinstall.com"
)
// Client for OpenInstall API
type Client struct {
apiKey string
httpClient *http.Client
}
// NewClient creates a new OpenInstall client
// apiKey: OpenInstall 数据接口 ApiKey
func NewClient(apiKey string) *Client {
return &Client{
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// PlatformStats represents statistics for a specific platform
type PlatformStats struct {
Platform string `json:"platform"`
Count int64 `json:"count"` // 下载/安装量
}
// PlatformDownloads 各端下载量统计
type PlatformDownloads struct {
Total int64 `json:"total"` // 总量
IOS int64 `json:"ios"` // iOS
Android int64 `json:"android"` // Android
Windows int64 `json:"windows"` // Windows
Mac int64 `json:"mac"` // Mac
Comparison *MonthComparison `json:"comparison"` // 环比数据
}
// MonthComparison 月度对比数据
type MonthComparison struct {
LastMonthTotal int64 `json:"lastMonthTotal"` // 上月总量
Change int64 `json:"change"` // 变化量 (正数=增长, 负数=下降)
ChangePercent float64 `json:"changePercent"` // 变化百分比
}
// APIResponse 通用响应结构
type APIResponse struct {
Code int `json:"code"`
Error *string `json:"error"`
Body json.RawMessage `json:"body"`
}
// GrowthData 新增安装数据
type GrowthData struct {
Date string `json:"date"`
Visit int64 `json:"visit"` // 访问量
Click int64 `json:"click"` // 点击量
Install int64 `json:"install"` // 安装量
Register int64 `json:"register"` // 注册量
SurviveD1 int64 `json:"survive_d1"` // 1日留存
SurviveD7 int64 `json:"survive_d7"` // 7日留存
SurviveD30 int64 `json:"survive_d30"` // 30日留存
}
// DistributionData 设备分布数据
type DistributionData struct {
Key string `json:"key"`
Value int64 `json:"value"`
}
// GetPlatformDownloads 获取各端下载量统计(当月数据 + 环比)
func (c *Client) GetPlatformDownloads(ctx context.Context, channel string) (*PlatformDownloads, error) {
now := time.Now()
// 当月数据本月1号到今天
startOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
endOfMonth := now
// 上月数据上月1号到上月最后一天
startOfLastMonth := startOfMonth.AddDate(0, -1, 0)
endOfLastMonth := startOfMonth.AddDate(0, 0, -1)
// 获取当月各平台数据
currentMonthData, err := c.getPlatformData(ctx, startOfMonth, endOfMonth, channel)
if err != nil {
return nil, fmt.Errorf("failed to get current month data: %w", err)
}
// 获取上月各平台数据
lastMonthData, err := c.getPlatformData(ctx, startOfLastMonth, endOfLastMonth, channel)
if err != nil {
return nil, fmt.Errorf("failed to get last month data: %w", err)
}
// 计算总量
currentTotal := currentMonthData.IOS + currentMonthData.Android + currentMonthData.Windows + currentMonthData.Mac
lastTotal := lastMonthData.IOS + lastMonthData.Android + lastMonthData.Windows + lastMonthData.Mac
// 计算环比
change := currentTotal - lastTotal
changePercent := float64(0)
if lastTotal > 0 {
changePercent = (float64(change) / float64(lastTotal)) * 100
}
return &PlatformDownloads{
Total: currentTotal,
IOS: currentMonthData.IOS,
Android: currentMonthData.Android,
Windows: currentMonthData.Windows,
Mac: currentMonthData.Mac,
Comparison: &MonthComparison{
LastMonthTotal: lastTotal,
Change: change,
ChangePercent: changePercent,
},
}, nil
}
// getPlatformData 获取指定时间范围内各平台的数据
func (c *Client) getPlatformData(ctx context.Context, startDate, endDate time.Time, channel string) (*PlatformDownloads, error) {
result := &PlatformDownloads{}
// 获取 iOS 数据
iosData, err := c.getDeviceDistribution(ctx, startDate, endDate, "ios", "total", channel)
if err != nil {
return nil, fmt.Errorf("failed to get iOS data: %w", err)
}
for _, item := range iosData {
result.IOS += item.Value
}
// 获取 Android 数据
androidData, err := c.getDeviceDistribution(ctx, startDate, endDate, "android", "total", channel)
if err != nil {
return nil, fmt.Errorf("failed to get Android data: %w", err)
}
for _, item := range androidData {
result.Android += item.Value
}
// Windows 和 Mac 暂时设为 0 (需要从其他数据源获取)
result.Windows = 0
result.Mac = 0
return result, nil
}
// getDeviceDistribution 获取设备分布数据
func (c *Client) getDeviceDistribution(ctx context.Context, startDate, endDate time.Time, platform, sumBy, channel string) ([]DistributionData, error) {
apiURL := fmt.Sprintf("%s/data/sum/growth", apiBaseURL)
params := url.Values{}
params.Add("apiKey", c.apiKey)
params.Add("beginDate", startDate.Format("2006-01-02"))
params.Add("endDate", endDate.Format("2006-01-02"))
params.Add("platform", platform)
params.Add("sumBy", sumBy)
if channel != "" {
params.Add("channelCode", channel)
}
params.Add("excludeDuplication", "0")
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 0 {
errMsg := "unknown error"
if apiResp.Error != nil {
errMsg = *apiResp.Error
}
return nil, fmt.Errorf("API error (code=%d): %s", apiResp.Code, errMsg)
}
var distData []DistributionData
if err := json.Unmarshal(apiResp.Body, &distData); err != nil {
return nil, fmt.Errorf("failed to parse distribution data: %w", err)
}
return distData, nil
}
// GetPlatformStats 获取平台统计数据(兼容旧接口)
func (c *Client) GetPlatformStats(ctx context.Context, startDate, endDate time.Time) ([]PlatformStats, error) {
// 调用新增安装数据接口
growthData, err := c.GetGrowthData(ctx, startDate, endDate, "total")
if err != nil {
return nil, fmt.Errorf("failed to get growth data: %w", err)
}
// 如果没有数据,返回空列表
if len(growthData) == 0 {
return []PlatformStats{}, nil
}
// 合并所有数据
var totalVisits, totalClicks int64
for _, data := range growthData {
totalVisits += data.Visit
totalClicks += data.Click
}
return []PlatformStats{
{
Platform: "All",
Count: totalVisits + totalClicks,
},
}, nil
}
// GetGrowthData 获取新增安装数据
func (c *Client) GetGrowthData(ctx context.Context, startDate, endDate time.Time, statType string) ([]GrowthData, error) {
apiURL := fmt.Sprintf("%s/data/event/growth", apiBaseURL)
params := url.Values{}
params.Add("apiKey", c.apiKey)
params.Add("startDate", startDate.Format("2006-01-02"))
params.Add("endDate", endDate.Format("2006-01-02"))
params.Add("statType", statType)
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var apiResp APIResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
if apiResp.Code != 0 {
errMsg := "unknown error"
if apiResp.Error != nil {
errMsg = *apiResp.Error
}
return nil, fmt.Errorf("API error (code=%d): %s", apiResp.Code, errMsg)
}
var growthData []GrowthData
if err := json.Unmarshal(apiResp.Body, &growthData); err != nil {
return nil, fmt.Errorf("failed to parse growth data: %w", err)
}
return growthData, nil
}

View File

@ -29,6 +29,8 @@ const (
InviteCodeError uint32 = 20009 InviteCodeError uint32 = 20009
// 邮箱绑定错误 // 邮箱绑定错误
EmailBindError uint32 = 20010 EmailBindError uint32 = 20010
// 邀请码已绑定
UserBindInviteCodeExist uint32 = 20011
) )
// Node error // Node error

View File

@ -35,6 +35,8 @@ func init() {
InviteCodeError: "Invite code error", InviteCodeError: "Invite code error",
// 邮箱绑定错误 // 邮箱绑定错误
EmailBindError: "已经绑定该邮箱", EmailBindError: "已经绑定该邮箱",
// 邀请码已绑定
UserBindInviteCodeExist: "已绑定他人",
// Node error // Node error
NodeExist: "Node already exists", NodeExist: "Node already exists",

BIN
ppanel

Binary file not shown.

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"strings"
"text/template" "text/template"
"time" "time"
@ -48,10 +49,33 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
var content string var content string
switch payload.Type { switch payload.Type {
case types.EmailTypeVerify: case types.EmailTypeVerify:
tpl, _ := template.New("verify").Parse(l.svcCtx.Config.Email.VerifyEmailTemplate) tplStr := l.svcCtx.Config.Email.VerifyEmailTemplate
var result bytes.Buffer
payload.Content["Type"] = uint8(payload.Content["Type"].(float64)) // Use int for better template compatibility
if t, ok := payload.Content["Type"].(float64); ok {
payload.Content["Type"] = int(t)
} else if t, ok := payload.Content["Type"].(int); ok {
payload.Content["Type"] = t
}
typeVal, _ := payload.Content["Type"].(int)
// Smart Fallback: If template is empty OR (Type is 4 but template doesn't support it), use default
// We check for "Type 4" or "Type eq 4" string in the template as a heuristic
needDefault := tplStr == ""
if !needDefault && typeVal == 4 &&
!strings.Contains(tplStr, "Type 4") &&
!strings.Contains(tplStr, "Type eq 4") {
logger.WithContext(ctx).Infow("[SendEmailLogic] Configured template might not support DeleteAccount (Type 4), forcing default template")
needDefault = true
}
if needDefault {
tplStr = email.DefaultEmailVerifyTemplate
}
tpl, _ := template.New("verify").Parse(tplStr)
var result bytes.Buffer
err = tpl.Execute(&result, payload.Content) err = tpl.Execute(&result, payload.Content)
if err != nil { if err != nil {
@ -63,7 +87,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
} }
content = result.String() content = result.String()
case types.EmailTypeMaintenance: case types.EmailTypeMaintenance:
tpl, _ := template.New("maintenance").Parse(l.svcCtx.Config.Email.MaintenanceEmailTemplate) tplStr := l.svcCtx.Config.Email.MaintenanceEmailTemplate
if tplStr == "" {
tplStr = email.DefaultMaintenanceEmailTemplate
}
tpl, _ := template.New("maintenance").Parse(tplStr)
var result bytes.Buffer var result bytes.Buffer
err = tpl.Execute(&result, payload.Content) err = tpl.Execute(&result, payload.Content)
if err != nil { if err != nil {
@ -76,7 +104,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
} }
content = result.String() content = result.String()
case types.EmailTypeExpiration: case types.EmailTypeExpiration:
tpl, _ := template.New("expiration").Parse(l.svcCtx.Config.Email.ExpirationEmailTemplate) tplStr := l.svcCtx.Config.Email.ExpirationEmailTemplate
if tplStr == "" {
tplStr = email.DefaultExpirationEmailTemplate
}
tpl, _ := template.New("expiration").Parse(tplStr)
var result bytes.Buffer var result bytes.Buffer
err = tpl.Execute(&result, payload.Content) err = tpl.Execute(&result, payload.Content)
if err != nil { if err != nil {
@ -89,7 +121,11 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
} }
content = result.String() content = result.String()
case types.EmailTypeTrafficExceed: case types.EmailTypeTrafficExceed:
tpl, _ := template.New("traffic_exceed").Parse(l.svcCtx.Config.Email.TrafficExceedEmailTemplate) tplStr := l.svcCtx.Config.Email.TrafficExceedEmailTemplate
if tplStr == "" {
tplStr = email.DefaultTrafficExceedEmailTemplate
}
tpl, _ := template.New("traffic_exceed").Parse(tplStr)
var result bytes.Buffer var result bytes.Buffer
err = tpl.Execute(&result, payload.Content) err = tpl.Execute(&result, payload.Content)
if err != nil { if err != nil {

View File

@ -67,22 +67,53 @@ func NewActivateOrderLogic(svc *svc.ServiceContext) *ActivateOrderLogic {
// It handles the complete workflow of activating a paid order including validation, // It handles the complete workflow of activating a paid order including validation,
// processing based on order type, and finalization. // processing based on order type, and finalization.
func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error { func (l *ActivateOrderLogic) ProcessTask(ctx context.Context, task *asynq.Task) error {
logger.WithContext(ctx).Info("[ActivateOrderLogic] 开始处理订单激活任务",
logger.Field("payload", string(task.Payload())))
payload, err := l.parsePayload(ctx, task.Payload()) payload, err := l.parsePayload(ctx, task.Payload())
if err != nil { if err != nil {
return nil // Log and continue logger.WithContext(ctx).Error("[ActivateOrderLogic] 解析 payload 失败,跳过任务",
logger.Field("error", err.Error()))
return nil // payload 解析失败不重试,因为重试也会失败
} }
logger.WithContext(ctx).Info("[ActivateOrderLogic] 正在验证订单",
logger.Field("order_no", payload.OrderNo))
orderInfo, err := l.validateAndGetOrder(ctx, payload.OrderNo) orderInfo, err := l.validateAndGetOrder(ctx, payload.OrderNo)
if err != nil { if err != nil {
return nil // Log and continue // 如果订单不存在或状态不对,不重试
if errors.Is(err, ErrInvalidOrderStatus) {
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单状态不是已支付,跳过",
logger.Field("order_no", payload.OrderNo))
return nil
}
// 数据库查询失败,应该重试
logger.WithContext(ctx).Error("[ActivateOrderLogic] 查询订单失败,将重试",
logger.Field("order_no", payload.OrderNo),
logger.Field("error", err.Error()))
return err
} }
logger.WithContext(ctx).Info("[ActivateOrderLogic] 订单验证通过,开始处理",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("order_type", orderInfo.Type),
logger.Field("user_id", orderInfo.UserId))
if err = l.processOrderByType(ctx, orderInfo); err != nil { if err = l.processOrderByType(ctx, orderInfo); err != nil {
logger.WithContext(ctx).Error("[ActivateOrderLogic] Process task failed", logger.Field("error", err.Error())) logger.WithContext(ctx).Error("[ActivateOrderLogic] 处理订单失败,将重试",
return nil logger.Field("order_no", orderInfo.OrderNo),
logger.Field("order_type", orderInfo.Type),
logger.Field("error", err.Error()))
return err // 返回 err 允许 asynq 重试
} }
l.finalizeCouponAndOrder(ctx, orderInfo) l.finalizeCouponAndOrder(ctx, orderInfo)
logger.WithContext(ctx).Info("[ActivateOrderLogic] ✅ 订单激活成功",
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("order_type", orderInfo.Type),
logger.Field("user_id", orderInfo.UserId))
return nil return nil
} }
@ -355,7 +386,7 @@ func (l *ActivateOrderLogic) createUserSubscription(ctx context.Context, orderIn
// This runs asynchronously to avoid blocking the main order processing flow. // This runs asynchronously to avoid blocking the main order processing flow.
func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) { func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *user.User, orderInfo *order.Order) {
if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) { if !l.shouldProcessCommission(userInfo, orderInfo.IsNew) {
l.grantGiftDaysToBothParties(ctx, userInfo) l.grantGiftDaysToBothParties(ctx, userInfo, orderInfo.OrderNo)
return return
} }
@ -423,12 +454,12 @@ func (l *ActivateOrderLogic) handleCommission(ctx context.Context, userInfo *use
} }
} }
func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User) { func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, referee *user.User, orderNo string) {
giftDays := l.svc.Config.Invite.GiftDays giftDays := l.svc.Config.Invite.GiftDays
if giftDays <= 0 || referee == nil || referee.Id == 0 { if giftDays <= 0 || referee == nil || referee.Id == 0 {
return return
} }
_ = l.grantGiftDays(ctx, referee, int(giftDays)) _ = l.grantGiftDays(ctx, referee, int(giftDays), orderNo, "邀请赠送")
if referee.RefererId == 0 { if referee.RefererId == 0 {
return return
} }
@ -436,10 +467,10 @@ func (l *ActivateOrderLogic) grantGiftDaysToBothParties(ctx context.Context, ref
if err != nil || referer == nil { if err != nil || referer == nil {
return return
} }
_ = l.grantGiftDays(ctx, referer, int(giftDays)) _ = l.grantGiftDays(ctx, referer, int(giftDays), orderNo, "邀请赠送")
} }
func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, days int) error { func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, days int, orderNo string, remark string) error {
if u == nil || days <= 0 { if u == nil || days <= 0 {
return nil return nil
} }
@ -451,7 +482,28 @@ func (l *ActivateOrderLogic) grantGiftDays(ctx context.Context, u *user.User, da
return err return err
} }
activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour) activeSubscribe.ExpireTime = activeSubscribe.ExpireTime.Add(time.Duration(days) * 24 * time.Hour)
return l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe) err = l.svc.UserModel.UpdateSubscribe(ctx, activeSubscribe)
if err != nil {
return err
}
// Insert system log
giftLog := &log.Gift{
Type: log.GiftTypeIncrease,
OrderNo: orderNo,
SubscribeId: activeSubscribe.Id,
Amount: int64(days),
Balance: u.Balance,
Remark: remark,
Timestamp: time.Now().UnixMilli(),
}
content, _ := giftLog.Marshal()
return l.svc.LogModel.Insert(ctx, &log.SystemLog{
Type: log.TypeGift.Uint8(),
Date: time.Now().Format("2006-01-02"),
ObjectID: u.Id,
Content: string(content),
})
} }
// shouldProcessCommission determines if commission should be processed based on // shouldProcessCommission determines if commission should be processed based on

17
scripts/check_invites.sql Normal file
View File

@ -0,0 +1,17 @@
-- 查看所有已绑定邀请关系的详细信息
-- 包含被邀请人ID、被邀请人账号(Email/Identifier)、推荐人ID、推荐人账号、绑定时间
SELECT
u.id AS '被邀请人ID (Invitee ID)',
(SELECT auth_identifier FROM user_auth_methods WHERE user_id = u.id LIMIT 1) AS '被邀请人账号 (Invitee Account)',
u.referer_id AS '推荐人ID (Referrer ID)',
(SELECT auth_identifier FROM user_auth_methods WHERE user_id = u.referer_id LIMIT 1) AS '推荐人账号 (Referrer Account)',
u.created_at AS '注册时间 (Registered At)'
FROM
user u
WHERE
u.referer_id > 0
ORDER BY
u.id DESC;
-- 如果只想看简单的计数统计
-- SELECT referer_id, count(*) as invite_count FROM user WHERE referer_id > 0 GROUP BY referer_id;

View File

@ -0,0 +1,14 @@
#!/bin/bash
# 批量清除用户缓存脚本
# 用法: ./clear_user_cache.sh
echo "正在清除所有用户缓存 (cache:user:id:*)..."
# 方法 1: 使用 xargs (适用于 Key 数量不是特别巨大的情况)
redis-cli KEYS "cache:user:id:*" | xargs -r redis-cli DEL
# 或者如果您的 Key 非常多,可以使用 SCAN 命令 (更安全,防堵塞)
# redis-cli --scan --pattern "cache:user:id:*" | xargs -r redis-cli DEL
echo "清除完成。"

View File

@ -0,0 +1,11 @@
-- 场景 1: 解绑邀请关系 (保留用户,只清除推荐人)
-- 将 {USER_ID} 替换为要解绑的“被邀请人”的用户 ID
UPDATE `user` SET `referer_id` = 0 WHERE `id` = {USER_ID};
-- 场景 2: 删除指定用户 (彻底删除用户及其关联数据)
-- 注意:删除用户通常涉及多张表,请谨慎操作。
-- 这里只提供基础删除,如果有外键约束可能会失败,建议在后台管理面板删除。
-- DELETE FROM `user` WHERE `id` = {USER_ID};
-- 场景 3: 清除某个用户的佣金记录 (如果需要重置)
-- DELETE FROM `system_logs` WHERE `type` = 33 AND `object_id` = {REFERRER_ID};

BIN
server

Binary file not shown.

76
test_agent_downloads.sh Executable file
View File

@ -0,0 +1,76 @@
#!/bin/bash
# 测试 OpenInstall Agent Downloads API
BASE_URL="https://tapi.hifast.biz"
API_PATH="/v1/public/user/agent/downloads"
echo "==============================================="
echo "测试 Agent Downloads API"
echo "==============================================="
# 如果您已经有 token请在这里设置
# 例如: TOKEN="your_jwt_token_here"
TOKEN=""
if [ -z "$TOKEN" ]; then
echo ""
echo "请提供用户 token 来测试此接口"
echo "您可以通过以下方式获取 token"
echo "1. 登录您的应用"
echo "2. 从浏览器开发者工具中复制 Authorization header 的 token"
echo ""
echo "或者使用邮箱/密码登录获取 token"
echo ""
read -p "请输入邮箱 (或直接按回车跳过): " EMAIL
if [ -n "$EMAIL" ]; then
read -sp "请输入密码: " PASSWORD
echo ""
# 尝试登录获取 token
LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/v1/auth/email/login" \
-H "Content-Type: application/json" \
-d "{\"email\":\"${EMAIL}\",\"password\":\"${PASSWORD}\"}")
echo "登录响应: $LOGIN_RESPONSE"
# 提取 token (需要 jq 工具,或者手动复制)
if command -v jq &> /dev/null; then
TOKEN=$(echo $LOGIN_RESPONSE | jq -r '.data.token // empty')
echo "提取到的 Token: $TOKEN"
else
echo "注意: 未安装 jq 工具,请手动从上面的响应中复制 token"
read -p "请粘贴 token: " TOKEN
fi
fi
fi
echo ""
echo "==============================================="
echo "调用 Agent Downloads API"
echo "==============================================="
if [ -z "$TOKEN" ]; then
echo "测试不带 token 的请求 (预期会失败)..."
curl -X GET "${BASE_URL}${API_PATH}" \
-H "Content-Type: application/json" \
-w "\n\nHTTP Status: %{http_code}\n"
else
echo "使用 Token 调用 API..."
curl -X GET "${BASE_URL}${API_PATH}" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${TOKEN}" \
-w "\n\nHTTP Status: %{http_code}\n"
fi
echo ""
echo "==============================================="
echo "当前 OpenInstall 配置:"
echo " Enable: true"
echo " AppKey: alf57p"
echo "==============================================="
echo ""
echo "注意: 当前 OpenInstall 集成使用的是 mock 数据"
echo "如需真实数据,需要实现 OpenInstall API 调用逻辑"
echo "文件位置: pkg/openinstall/openinstall.go"

167
test_data_mock_invites.sql Normal file
View File

@ -0,0 +1,167 @@
-- ===================================================================
-- 模拟邀请数据 SQL - 用于测试 Agent/Invite APIs
-- ===================================================================
-- 说明:
-- 1. 假设当前登录用户 ID = 524邀请者
-- 2. 创建 5 个被邀请用户user_id: 10001-10005
-- 3. 其中 3 个用户有付费订单user_id: 10001, 10002, 10003
-- 4. 2 个用户没有订单user_id: 10004, 10005
-- ===================================================================
-- 1. 插入被邀请用户referer_id = 524即当前登录用户
-- 注意:如果这些 ID 已存在,请先手动修改为其他未使用的 ID
-- 用户 10001有2个已支付订单
INSERT INTO `user` (
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
`balance`, `commission`, `created_at`, `updated_at`
) VALUES (
10001, 524, 'MOCK_CODE_01', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
0, 0, NOW(), NOW()
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
-- 为用户 10001 添加 email 认证方式
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
VALUES (10001, 'email', 'mock_user_001@test.com', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_001@test.com';
-- 用户 10002有1个已支付订单
INSERT INTO `user` (
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
`balance`, `commission`, `created_at`, `updated_at`
) VALUES (
10002, 524, 'MOCK_CODE_02', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
0, 0, NOW(), NOW()
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
VALUES (10002, 'email', 'mock_user_002@test.com', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_002@test.com';
-- 用户 10003有1个已完成订单
INSERT INTO `user` (
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
`balance`, `commission`, `created_at`, `updated_at`
) VALUES (
10003, 524, 'MOCK_CODE_03', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
0, 0, NOW(), NOW()
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
VALUES (10003, 'email', 'mock_user_003@test.com', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_003@test.com';
-- 用户 10004没有订单历史邀请但未付费
INSERT INTO `user` (
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
`balance`, `commission`, `created_at`, `updated_at`
) VALUES (
10004, 524, 'MOCK_CODE_04', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
0, 0, NOW(), NOW()
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
VALUES (10004, 'email', 'mock_user_004@test.com', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_004@test.com';
-- 用户 10005没有订单历史邀请但未付费
INSERT INTO `user` (
`id`, `referer_id`, `refer_code`, `password`, `is_admin`,
`balance`, `commission`, `created_at`, `updated_at`
) VALUES (
10005, 524, 'MOCK_CODE_05', '$2a$10$mock.hash.for.testing.only.password.bcrypt.hash', 0,
0, 0, NOW(), NOW()
) ON DUPLICATE KEY UPDATE `referer_id` = 524;
INSERT INTO `user_auth_methods` (`user_id`, `auth_type`, `auth_identifier`, `verified`, `created_at`, `updated_at`)
VALUES (10005, 'email', 'mock_user_005@test.com', 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE `auth_identifier` = 'mock_user_005@test.com';
-- ===================================================================
-- 2. 插入订单数据
-- status: 2 = Paid已支付, 5 = Finished已完成
-- ===================================================================
-- 订单 1用户 10001已支付金额 $9.99,佣金 $0.99
INSERT INTO `order` (
`user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`,
`payment_id`, `created_at`, `updated_at`
) VALUES (
10001, 'MOCK_ORDER_001', 1, 2, 999, 99, 1,
1, DATE_SUB(NOW(), INTERVAL 10 DAY), DATE_SUB(NOW(), INTERVAL 10 DAY)
);
-- 订单 2用户 10001已支付金额 $19.99,佣金 $1.99
INSERT INTO `order` (
`user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`,
`payment_id`, `created_at`, `updated_at`
) VALUES (
10001, 'MOCK_ORDER_002', 1, 2, 1999, 199, 1,
1, DATE_SUB(NOW(), INTERVAL 5 DAY), DATE_SUB(NOW(), INTERVAL 5 DAY)
);
-- 订单 3用户 10002已支付金额 $29.99,佣金 $2.99
INSERT INTO `order` (
`user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`,
`payment_id`, `created_at`, `updated_at`
) VALUES (
10002, 'MOCK_ORDER_003', 1, 2, 2999, 299, 1,
1, DATE_SUB(NOW(), INTERVAL 3 DAY), DATE_SUB(NOW(), INTERVAL 3 DAY)
);
-- 订单 4用户 10003已完成金额 $49.99,佣金 $4.99
INSERT INTO `order` (
`user_id`, `order_no`, `type`, `status`, `amount`, `commission`, `quantity`,
`payment_id`, `created_at`, `updated_at`
) VALUES (
10003, 'MOCK_ORDER_004', 1, 5, 4999, 499, 1,
1, DATE_SUB(NOW(), INTERVAL 1 DAY), DATE_SUB(NOW(), INTERVAL 1 DAY)
);
-- 订单 5用户 10001待支付status=1不会被统计
INSERT INTO `order` (
`user_id`, `order_no`, `type`, `status`, `amount`, `quantity`,
`payment_id`, `created_at`, `updated_at`
) VALUES (
10001, 'MOCK_ORDER_005', 1, 1, 999, 1,
1, NOW(), NOW()
);
-- ===================================================================
-- 3. 验证查询
-- ===================================================================
-- 查询当前用户(524)的邀请统计
-- 应返回:
-- - friendlyCount: 3 (user_id 10001, 10002, 10003 有已支付/完成订单)
-- - historyCount: 5 (user_id 10001-10005 都是被邀请用户)
SELECT
(SELECT COUNT(*) FROM user WHERE referer_id = 524
AND EXISTS (SELECT 1 FROM `order` o WHERE o.user_id = user.id AND o.status IN (2, 5))
) as friendlyCount,
(SELECT COUNT(*) FROM user WHERE referer_id = 524) as historyCount;
-- 查询销售记录分页查询前10条
-- 应返回 4 条记录MOCK_ORDER_001 到 MOCK_ORDER_004
SELECT
o.amount,
UNIX_TIMESTAMP(o.created_at) * 1000 as created_at,
u.id as user_id,
COALESCE(am.auth_identifier, 'no_email') as user_email
FROM `order` o
JOIN user u ON o.user_id = u.id
LEFT JOIN user_auth_methods am ON am.user_id = u.id AND am.auth_type = 'email'
WHERE u.referer_id = 524 AND o.status IN (2, 5)
ORDER BY o.created_at DESC
LIMIT 10 OFFSET 0;
-- ===================================================================
-- 清理命令(如需删除测试数据,请执行以下语句)
-- ===================================================================
-- DELETE FROM `order` WHERE order_no LIKE 'MOCK_ORDER_%';
-- DELETE FROM `user` WHERE id BETWEEN 10001 AND 10005;

View File

@ -28,3 +28,6 @@
- [2026-01-13] **设备移出和邀请码优化** - [2026-01-13] **设备移出和邀请码优化**
- 修复设备B绑定邮箱后被从设备A移除时未自动退出的问题通过踢出旧连接和清理缓存实现 - 修复设备B绑定邮箱后被从设备A移除时未自动退出的问题通过踢出旧连接和清理缓存实现
- 优化邀请码无效时的错误提示,返回 "无邀请码" - 优化邀请码无效时的错误提示,返回 "无邀请码"
- [2026-01-26] **优化下载链接生成**
- 修复未携带邀请码时 URL 中多余 `-ic_` 的问题
- 增加单元测试确保逻辑正确