203
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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. **测试验证**: 重新运行测试程序验证数据获取
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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("========================================")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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("========================================")
|
||||
}
|
||||
Reference in New Issue
Block a user