first commit
This commit is contained in:
47
mengyaping-backend/.dockerignore
Normal file
47
mengyaping-backend/.dockerignore
Normal file
@@ -0,0 +1,47 @@
|
||||
# Git 相关
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# 编辑器和 IDE
|
||||
.vscode
|
||||
.idea
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 数据文件(运行时生成)
|
||||
data/*.json
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
|
||||
# 临时文件
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# 文档
|
||||
README.md
|
||||
LICENSE
|
||||
*.md
|
||||
|
||||
# Docker 相关
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
|
||||
# 测试文件
|
||||
*_test.go
|
||||
test/
|
||||
tests/
|
||||
|
||||
# 构建产物
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
56
mengyaping-backend/Dockerfile
Normal file
56
mengyaping-backend/Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
||||
# 多阶段构建 - 使用官方 Golang 镜像作为构建环境
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装必要的构建工具
|
||||
RUN apk add --no-cache git ca-certificates tzdata
|
||||
|
||||
# 复制 go.mod 和 go.sum 文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用程序
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o mengyaping-backend .
|
||||
|
||||
# 使用轻量级的 alpine 镜像作为运行环境
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装必要的运行时依赖
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区为上海
|
||||
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1000 appuser && \
|
||||
adduser -D -u 1000 -G appuser appuser
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制编译好的二进制文件
|
||||
COPY --from=builder /app/mengyaping-backend .
|
||||
|
||||
# 创建数据目录
|
||||
RUN mkdir -p /app/data && chown -R appuser:appuser /app
|
||||
|
||||
# 切换到非 root 用户
|
||||
USER appuser
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1
|
||||
|
||||
# 运行应用程序
|
||||
CMD ["./mengyaping-backend"]
|
||||
176
mengyaping-backend/config/config.go
Normal file
176
mengyaping-backend/config/config.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config 应用配置
|
||||
type Config struct {
|
||||
Server ServerConfig `json:"server"`
|
||||
Monitor MonitorConfig `json:"monitor"`
|
||||
DataPath string `json:"data_path"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务器配置
|
||||
type ServerConfig struct {
|
||||
Port string `json:"port"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
// MonitorConfig 监控配置
|
||||
type MonitorConfig struct {
|
||||
Interval time.Duration `json:"interval"` // 检测间隔
|
||||
Timeout time.Duration `json:"timeout"` // 请求超时时间
|
||||
RetryCount int `json:"retry_count"` // 重试次数
|
||||
HistoryDays int `json:"history_days"` // 保留历史天数
|
||||
}
|
||||
|
||||
var (
|
||||
cfg *Config
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetConfig 获取配置单例
|
||||
func GetConfig() *Config {
|
||||
once.Do(func() {
|
||||
cfg = &Config{
|
||||
Server: ServerConfig{
|
||||
Port: getEnv("SERVER_PORT", "8080"),
|
||||
Host: getEnv("SERVER_HOST", "0.0.0.0"),
|
||||
},
|
||||
Monitor: MonitorConfig{
|
||||
Interval: parseDuration(getEnv("MONITOR_INTERVAL", "5m"), 5*time.Minute),
|
||||
Timeout: parseDuration(getEnv("MONITOR_TIMEOUT", "10s"), 10*time.Second),
|
||||
RetryCount: parseInt(getEnv("MONITOR_RETRY_COUNT", "3"), 3),
|
||||
HistoryDays: parseInt(getEnv("MONITOR_HISTORY_DAYS", "7"), 7),
|
||||
},
|
||||
DataPath: getEnv("DATA_PATH", "./data"),
|
||||
}
|
||||
|
||||
// 尝试从配置文件加载(会覆盖环境变量配置)
|
||||
loadConfigFromFile()
|
||||
})
|
||||
return cfg
|
||||
}
|
||||
|
||||
// getEnv 获取环境变量,如果不存在则返回默认值
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// parseInt 解析整数环境变量
|
||||
func parseInt(value string, defaultValue int) int {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
var result int
|
||||
if _, err := fmt.Sscanf(value, "%d", &result); err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// parseDuration 解析时间间隔环境变量
|
||||
func parseDuration(value string, defaultValue time.Duration) time.Duration {
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
if duration, err := time.ParseDuration(value); err == nil {
|
||||
return duration
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// loadConfigFromFile 从文件加载配置
|
||||
func loadConfigFromFile() {
|
||||
configFile := "./data/config.json"
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var fileCfg struct {
|
||||
Server ServerConfig `json:"server"`
|
||||
Monitor struct {
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
HistoryDays int `json:"history_days"`
|
||||
} `json:"monitor"`
|
||||
DataPath string `json:"data_path"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &fileCfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if fileCfg.Server.Port != "" {
|
||||
cfg.Server.Port = fileCfg.Server.Port
|
||||
}
|
||||
if fileCfg.Server.Host != "" {
|
||||
cfg.Server.Host = fileCfg.Server.Host
|
||||
}
|
||||
if fileCfg.Monitor.IntervalMinutes > 0 {
|
||||
cfg.Monitor.Interval = time.Duration(fileCfg.Monitor.IntervalMinutes) * time.Minute
|
||||
}
|
||||
if fileCfg.Monitor.TimeoutSeconds > 0 {
|
||||
cfg.Monitor.Timeout = time.Duration(fileCfg.Monitor.TimeoutSeconds) * time.Second
|
||||
}
|
||||
if fileCfg.Monitor.RetryCount > 0 {
|
||||
cfg.Monitor.RetryCount = fileCfg.Monitor.RetryCount
|
||||
}
|
||||
if fileCfg.Monitor.HistoryDays > 0 {
|
||||
cfg.Monitor.HistoryDays = fileCfg.Monitor.HistoryDays
|
||||
}
|
||||
if fileCfg.DataPath != "" {
|
||||
cfg.DataPath = fileCfg.DataPath
|
||||
}
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置到文件
|
||||
func SaveConfig() error {
|
||||
configFile := cfg.DataPath + "/config.json"
|
||||
|
||||
fileCfg := struct {
|
||||
Server ServerConfig `json:"server"`
|
||||
Monitor struct {
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
HistoryDays int `json:"history_days"`
|
||||
} `json:"monitor"`
|
||||
DataPath string `json:"data_path"`
|
||||
}{
|
||||
Server: cfg.Server,
|
||||
Monitor: struct {
|
||||
IntervalMinutes int `json:"interval_minutes"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
RetryCount int `json:"retry_count"`
|
||||
HistoryDays int `json:"history_days"`
|
||||
}{
|
||||
IntervalMinutes: int(cfg.Monitor.Interval.Minutes()),
|
||||
TimeoutSeconds: int(cfg.Monitor.Timeout.Seconds()),
|
||||
RetryCount: cfg.Monitor.RetryCount,
|
||||
HistoryDays: cfg.Monitor.HistoryDays,
|
||||
},
|
||||
DataPath: cfg.DataPath,
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(fileCfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(configFile, data, 0644)
|
||||
}
|
||||
13
mengyaping-backend/data/config.json
Normal file
13
mengyaping-backend/data/config.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"server": {
|
||||
"port": "8080",
|
||||
"host": "0.0.0.0"
|
||||
},
|
||||
"monitor": {
|
||||
"interval_minutes": 5,
|
||||
"timeout_seconds": 10,
|
||||
"retry_count": 3,
|
||||
"history_days": 7
|
||||
},
|
||||
"data_path": "./data"
|
||||
}
|
||||
18
mengyaping-backend/data/groups.json
Normal file
18
mengyaping-backend/data/groups.json
Normal file
@@ -0,0 +1,18 @@
|
||||
[
|
||||
{
|
||||
"id": "self-made",
|
||||
"name": "自制网站"
|
||||
},
|
||||
{
|
||||
"id": "self-deploy",
|
||||
"name": "自部署网站"
|
||||
},
|
||||
{
|
||||
"id": "admin",
|
||||
"name": "管理员网站"
|
||||
},
|
||||
{
|
||||
"id": "api",
|
||||
"name": "API网站"
|
||||
}
|
||||
]
|
||||
1283
mengyaping-backend/data/records.json
Normal file
1283
mengyaping-backend/data/records.json
Normal file
File diff suppressed because it is too large
Load Diff
626
mengyaping-backend/data/websites.json
Normal file
626
mengyaping-backend/data/websites.json
Normal file
@@ -0,0 +1,626 @@
|
||||
[
|
||||
{
|
||||
"id": "188cb48c619bd5d896bff546",
|
||||
"name": "萌芽主页",
|
||||
"group": "self-made",
|
||||
"urls": [
|
||||
{
|
||||
"id": "d19b0a475334",
|
||||
"url": "https://shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://shumengya.top/favicon.ico",
|
||||
"title": "萌芽主页",
|
||||
"created_at": "2026-01-21T17:21:08.6757862+08:00",
|
||||
"updated_at": "2026-01-27T13:02:31.4021667+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb49298d72b78a37301a1",
|
||||
"name": "萌芽备忘录",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "9d25ae024f36",
|
||||
"url": "https://memos.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://memos.shumengya.top/logo.webp",
|
||||
"title": "Memos",
|
||||
"created_at": "2026-01-21T17:21:35.3722254+08:00",
|
||||
"updated_at": "2026-01-27T13:02:37.9135994+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb52c04cf639cfc2c8ba4",
|
||||
"name": "萌芽盘",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "cfe6229fb86b",
|
||||
"url": "https://pan.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://img.shumengya.top/i/2026/01/04/695a660870959.png",
|
||||
"title": "萌芽盘",
|
||||
"created_at": "2026-01-21T17:32:34.3136511+08:00",
|
||||
"updated_at": "2026-01-27T13:02:42.907207+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb53623f46b7872bf17b6",
|
||||
"name": "萌芽Git仓库",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "b53594e7285d",
|
||||
"url": "https://git.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://git.shumengya.top/assets/img/favicon.svg",
|
||||
"title": "萌芽Git仓库",
|
||||
"created_at": "2026-01-21T17:33:17.7858446+08:00",
|
||||
"updated_at": "2026-01-27T13:03:13.0909987+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb54b93ddd1e487bf347e",
|
||||
"name": "萌芽图床",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "46d822694242",
|
||||
"url": "https://image.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://image.shumengya.top/favicon.ico",
|
||||
"title": "萌芽图床",
|
||||
"created_at": "2026-01-21T17:34:49.8577249+08:00",
|
||||
"updated_at": "2026-01-27T13:03:19.4761768+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb5507dcd2da0559bd6dd",
|
||||
"name": "萌芽笔记",
|
||||
"group": "self-made",
|
||||
"urls": [
|
||||
{
|
||||
"id": "6d5125db9d81",
|
||||
"url": "https://note.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://note.shumengya.top/logo.png",
|
||||
"title": "萌芽笔记",
|
||||
"created_at": "2026-01-21T17:35:10.962372+08:00",
|
||||
"updated_at": "2026-01-27T13:03:25.8837656+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb55693193b80d2383cc3",
|
||||
"name": "萌芽作品集",
|
||||
"group": "self-made",
|
||||
"urls": [
|
||||
{
|
||||
"id": "74a3b6e1f50e",
|
||||
"url": "https://work.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://work.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T17:35:37.0894816+08:00",
|
||||
"updated_at": "2026-01-27T13:03:30.0659765+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb56d8df57dc8c66d42ee",
|
||||
"name": "万象口袋",
|
||||
"group": "self-made",
|
||||
"urls": [
|
||||
{
|
||||
"id": "e8f8fd4cf9b9",
|
||||
"url": "https://infogenie.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://infogenie.shumengya.top/assets/logo.png",
|
||||
"title": "万象口袋",
|
||||
"created_at": "2026-01-21T17:37:15.787501+08:00",
|
||||
"updated_at": "2026-01-27T13:03:34.8680468+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb574b122f71087e53ae1",
|
||||
"name": "萌芽问卷",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "2f9ffdedd860",
|
||||
"url": "https://survey.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://survey.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T17:37:46.4424548+08:00",
|
||||
"updated_at": "2026-01-27T13:03:38.9707881+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb5a19b836e80d93df86a",
|
||||
"name": "萌芽短链",
|
||||
"group": "self-made",
|
||||
"urls": [
|
||||
{
|
||||
"id": "8a4ebcb7eb03",
|
||||
"url": "https://short.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://short.shumengya.top/logo.png",
|
||||
"title": "萌芽短链",
|
||||
"created_at": "2026-01-21T17:40:59.3532064+08:00",
|
||||
"updated_at": "2026-01-27T13:04:16.9980537+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb6da20586d34df88b086",
|
||||
"name": "萌芽漂流瓶",
|
||||
"group": "self-made",
|
||||
"urls": [
|
||||
{
|
||||
"id": "e7e250eb8985",
|
||||
"url": "https://bottle.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://bottle.shumengya.top/logo.png",
|
||||
"title": "萌芽漂流瓶(´,,•ω•,,)♡",
|
||||
"created_at": "2026-01-21T18:03:21.6115541+08:00",
|
||||
"updated_at": "2026-01-27T13:04:27.0426589+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb6ff716e2590408704ee",
|
||||
"name": "编程速查表[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "85620784f1a3",
|
||||
"url": "https://reference.smyhub.com",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://reference.smyhub.com/icons/favicon.svg",
|
||||
"title": "Quick Reference\n \u0026#x26; Quick Reference",
|
||||
"created_at": "2026-01-21T18:06:01.885722+08:00",
|
||||
"updated_at": "2026-01-27T13:10:39.3541213+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb708e8cf52a49a436570",
|
||||
"name": "思绪思维导图[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "dcc07e30a25a",
|
||||
"url": "https://mind-map.smyhub.com",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://mind-map.smyhub.com/dist/logo.ico",
|
||||
"title": "思绪思维导图",
|
||||
"created_at": "2026-01-21T18:06:42.5432849+08:00",
|
||||
"updated_at": "2026-01-27T13:10:46.0879149+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb71029ad43105a3dc6c0",
|
||||
"name": "it-tools工具集[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "f278e65dd498",
|
||||
"url": "https://it-tools.smyhub.com",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://it-tools.smyhub.com/favicon.ico",
|
||||
"title": "IT Tools - Handy online tools for developers",
|
||||
"created_at": "2026-01-21T18:07:13.6963428+08:00",
|
||||
"updated_at": "2026-01-27T13:11:45.7629645+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb71b527c48bc3c344acc",
|
||||
"name": "xtools工具集[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "13cdc2afab17",
|
||||
"url": "https://xtools.smyhub.com",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://xtools.smyhub.com/favicon.ico",
|
||||
"title": "百川云常用工具",
|
||||
"created_at": "2026-01-21T18:08:01.6256391+08:00",
|
||||
"updated_at": "2026-01-27T13:12:18.9167742+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb72555e8d5c4f0d7aa98",
|
||||
"name": "萌芽监控面板",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "421fe08ece5e",
|
||||
"url": "http://monitor.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "http://monitor.shumengya.top/logo.png",
|
||||
"title": "萌芽监控面板",
|
||||
"created_at": "2026-01-21T18:08:44.6327577+08:00",
|
||||
"updated_at": "2026-01-23T22:58:04.325685258+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb73272be1c9c5a5d6812",
|
||||
"name": "在线office",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "cba98c3c329b",
|
||||
"url": "https://office.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://office.shumengya.top/img/64.png",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:09:40.9510719+08:00",
|
||||
"updated_at": "2026-02-10T01:28:12.7674442+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb7372655f3841e82a210",
|
||||
"name": "网页魔方[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "ed4c7dcd0565",
|
||||
"url": "https://cube.smyhub.com",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://cube.smyhub.com/favicon.ico",
|
||||
"title": "HTML5 3D魔方小游戏",
|
||||
"created_at": "2026-01-21T18:10:01.1440137+08:00",
|
||||
"updated_at": "2026-01-27T13:12:55.4496439+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb764a98dbb2453c14b8f",
|
||||
"name": "60sAPI集合",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "2f0596587997",
|
||||
"url": "https://60s.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://60s.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:13:16.6194034+08:00",
|
||||
"updated_at": "2026-01-26T11:56:41.21983174+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb77f231fe68c73e24b29",
|
||||
"name": "萌芽主页后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "c182709178d9",
|
||||
"url": "https://nav.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://nav.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:15:10.3277851+08:00",
|
||||
"updated_at": "2026-01-26T11:56:41.237091652+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb789a76896788f139a96",
|
||||
"name": "萌芽笔记后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "2b42ed612dd3",
|
||||
"url": "https://note.api.shumengya.top/api/tree",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://note.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:15:55.4968142+08:00",
|
||||
"updated_at": "2026-01-26T11:56:40.772396513+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb7934d5d413890dea61f",
|
||||
"name": "萌芽作品集后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "d841646c7b41",
|
||||
"url": "https://work.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://work.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:16:36.935795+08:00",
|
||||
"updated_at": "2026-01-26T11:56:40.714095868+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb79ed2aeb85cabcf6e66",
|
||||
"name": "万象口袋后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "7c8ff0e48be4",
|
||||
"url": "https://infogenie.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://infogenie.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:17:26.4171439+08:00",
|
||||
"updated_at": "2026-01-26T11:56:41.067849322+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb7b7b154ddbc1dc4b14d",
|
||||
"name": "萌芽短链后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "ef0443476dd8",
|
||||
"url": "https://short.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://short.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:19:13.2317895+08:00",
|
||||
"updated_at": "2026-01-26T11:56:41.107775414+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cb7c8586dd2c4dcf4bd1e",
|
||||
"name": "萌芽漂流瓶后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "432740a25595",
|
||||
"url": "https://bottle.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://bottle.api.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-21T18:20:24.7546969+08:00",
|
||||
"updated_at": "2026-01-26T11:56:41.019334673+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf6ed769b75e497cd2a4a",
|
||||
"name": "大萌芽1Panel面板[WG]",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "a3e3ff7839f1",
|
||||
"url": "http://10.0.0.233:19132/smy",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "http://10.0.0.233:19132/favicon.ico",
|
||||
"title": "loading...",
|
||||
"created_at": "2026-01-22T13:37:33.4073441+08:00",
|
||||
"updated_at": "2026-02-10T01:33:12.8434302+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf6f322f76b90c35c3484",
|
||||
"name": "小萌芽1Panel面板[WG]",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "76b6f9fe4f04",
|
||||
"url": "http://10.0.0.100:19132/smy",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "http://10.0.0.100:19132/favicon.ico",
|
||||
"title": "loading...",
|
||||
"created_at": "2026-01-22T13:37:57.7738884+08:00",
|
||||
"updated_at": "2026-02-10T01:33:17.4968298+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf7019e56e618827f4927",
|
||||
"name": "大萌芽Portaintor面板[WG]",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "093d47789bb5",
|
||||
"url": "http://10.0.0.233:8484/",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "http://10.0.0.233:8484/dd5d4c0b208895c5a7de.png",
|
||||
"title": "Portainer",
|
||||
"created_at": "2026-01-22T13:38:59.9732854+08:00",
|
||||
"updated_at": "2026-02-10T01:38:15.5238829+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf70700922fa4041f6470",
|
||||
"name": "小萌芽Portaintor面板[WG]",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "777c292c7696",
|
||||
"url": "http://10.0.0.100:8484/",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "http://10.0.0.100:8484/dd5d4c0b208895c5a7de.png",
|
||||
"title": "Portainer",
|
||||
"created_at": "2026-01-22T13:39:23.0961745+08:00",
|
||||
"updated_at": "2026-02-10T01:38:36.0226948+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf728ed2992f06a8f9e52",
|
||||
"name": "easytier面板",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "0a0a59069228",
|
||||
"url": "http://easytier.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "http://easytier.shumengya.top/easytier.png",
|
||||
"title": "EasyTier Dashboard",
|
||||
"created_at": "2026-01-22T13:41:48.7994396+08:00",
|
||||
"updated_at": "2026-01-23T22:58:03.667100466+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf72edea71c4cb2284c93",
|
||||
"name": "wireguard面板",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "170fab2239ad",
|
||||
"url": "https://wireguard.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://wireguard.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-22T13:42:14.3258123+08:00",
|
||||
"updated_at": "2026-01-26T11:56:40.996902062+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf746b16a83dc4d5e3d13",
|
||||
"name": "萌芽Docker镜像仓库",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "822329e621c2",
|
||||
"url": "https://repo.docker.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://repo.docker.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-22T13:43:56.6460815+08:00",
|
||||
"updated_at": "2026-01-26T11:56:40.972109897+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188cf74b2fa5da7c8e2b588e",
|
||||
"name": "萌芽通知",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "2f580f90f979",
|
||||
"url": "https://notice.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://notice.shumengya.top/favicon.ico",
|
||||
"title": "502 Bad Gateway",
|
||||
"created_at": "2026-01-22T13:44:15.9437687+08:00",
|
||||
"updated_at": "2026-01-26T11:56:41.065412821+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188d00db054cf7860dff3440",
|
||||
"name": "萌芽Ping",
|
||||
"group": "admin",
|
||||
"urls": [
|
||||
{
|
||||
"id": "ac85b166ccc4",
|
||||
"url": "https://ping.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://ping.shumengya.top/favicon.ico",
|
||||
"title": "萌芽Ping - 网站监控面板",
|
||||
"created_at": "2026-01-22T16:39:29.31324815+08:00",
|
||||
"updated_at": "2026-01-23T13:28:03.633565425+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188d00edf571226f42ec8b20",
|
||||
"name": "萌芽Ping后端API",
|
||||
"group": "api",
|
||||
"urls": [
|
||||
{
|
||||
"id": "0bf0fe1bb9cc",
|
||||
"url": "https://ping.api.shumengya.top",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://ping.api.shumengya.top/favicon.ico",
|
||||
"title": "萌芽Git仓库",
|
||||
"created_at": "2026-01-22T16:40:50.651561641+08:00",
|
||||
"updated_at": "2026-01-23T13:28:03.351764738+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188e7f1a40cdc7bc3bba1817",
|
||||
"name": "DNS查询[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "9af024b3d387",
|
||||
"url": "https://cf-dns.smyhub.com/",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://cf-assets.www.cloudflare.com/dzlvafdwdttg/6TaQ8Q7BDmdAFRoHpDCb82/8d9bc52a2ac5af100de3a9adcf99ffaa/security-shield-protection-2.svg",
|
||||
"title": "DNS-over-HTTPS Resolver",
|
||||
"created_at": "2026-01-27T13:24:14.3362887+08:00",
|
||||
"updated_at": "2026-01-27T13:24:15.171393+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188e7f27a909fe247a2fcd15",
|
||||
"name": "Github文件加速[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "b897ef0def67",
|
||||
"url": "https://gh-proxy.smyhub.com/",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://gh-proxy.smyhub.com/favicon.ico",
|
||||
"title": "GitHub 文件加速",
|
||||
"created_at": "2026-01-27T13:25:11.9196401+08:00",
|
||||
"updated_at": "2026-01-27T13:25:12.8034353+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188e7f33f087667c29da05f2",
|
||||
"name": "FloppyBird[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "75a0cb223c2d",
|
||||
"url": "https://floppy-bird.smyhub.com/",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://floppy-bird.smyhub.com/favicon.ico",
|
||||
"title": "Floppy Bird",
|
||||
"created_at": "2026-01-27T13:26:04.6586487+08:00",
|
||||
"updated_at": "2026-01-27T13:26:05.4749231+08:00"
|
||||
},
|
||||
{
|
||||
"id": "188e7f4cdd13a3bc80c68184",
|
||||
"name": "别踩白方块[CloudFlare]",
|
||||
"group": "self-deploy",
|
||||
"urls": [
|
||||
{
|
||||
"id": "9f11daaedba4",
|
||||
"url": "https://floppy-bird.smyhub.com/",
|
||||
"remark": ""
|
||||
}
|
||||
],
|
||||
"favicon": "https://floppy-bird.smyhub.com/favicon.ico",
|
||||
"title": "Floppy Bird",
|
||||
"created_at": "2026-01-27T13:27:51.7064775+08:00",
|
||||
"updated_at": "2026-01-27T13:27:51.9331276+08:00"
|
||||
}
|
||||
]
|
||||
40
mengyaping-backend/docker-compose.yml
Normal file
40
mengyaping-backend/docker-compose.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mengyaping-backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mengyaping-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "6161:8080"
|
||||
volumes:
|
||||
# 持久化数据目录
|
||||
- /shumengya/docker/mengyaping-backend/data/:/app/data
|
||||
environment:
|
||||
# 服务器配置
|
||||
- SERVER_PORT=8080
|
||||
- SERVER_HOST=0.0.0.0
|
||||
# 监控配置
|
||||
- MONITOR_INTERVAL=5m
|
||||
- MONITOR_TIMEOUT=10s
|
||||
- MONITOR_RETRY_COUNT=3
|
||||
- MONITOR_HISTORY_DAYS=7
|
||||
networks:
|
||||
- mengyaping-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/health"]
|
||||
interval: 30s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
mengyaping-network:
|
||||
driver: bridge
|
||||
42
mengyaping-backend/go.mod
Normal file
42
mengyaping-backend/go.mod
Normal file
@@ -0,0 +1,42 @@
|
||||
module mengyaping-backend
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.25.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/tools v0.34.0 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
)
|
||||
91
mengyaping-backend/go.sum
Normal file
91
mengyaping-backend/go.sum
Normal file
@@ -0,0 +1,91 @@
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
|
||||
github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
199
mengyaping-backend/handlers/website.go
Normal file
199
mengyaping-backend/handlers/website.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/services"
|
||||
)
|
||||
|
||||
// WebsiteHandler 网站处理器
|
||||
type WebsiteHandler struct {
|
||||
websiteService *services.WebsiteService
|
||||
monitorService *services.MonitorService
|
||||
}
|
||||
|
||||
// NewWebsiteHandler 创建网站处理器
|
||||
func NewWebsiteHandler() *WebsiteHandler {
|
||||
return &WebsiteHandler{
|
||||
websiteService: services.NewWebsiteService(),
|
||||
monitorService: services.GetMonitorService(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebsites 获取所有网站状态
|
||||
func (h *WebsiteHandler) GetWebsites(c *gin.Context) {
|
||||
statuses := h.monitorService.GetAllWebsiteStatuses()
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: statuses,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWebsite 获取单个网站状态
|
||||
func (h *WebsiteHandler) GetWebsite(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
status := h.monitorService.GetWebsiteStatus(id)
|
||||
if status == nil {
|
||||
c.JSON(http.StatusNotFound, models.APIResponse{
|
||||
Code: 404,
|
||||
Message: "网站不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: status,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateWebsite 创建网站
|
||||
func (h *WebsiteHandler) CreateWebsite(c *gin.Context) {
|
||||
var req models.CreateWebsiteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIResponse{
|
||||
Code: 400,
|
||||
Message: "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
website, err := h.websiteService.CreateWebsite(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIResponse{
|
||||
Code: 500,
|
||||
Message: "创建失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "创建成功",
|
||||
Data: website,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateWebsite 更新网站
|
||||
func (h *WebsiteHandler) UpdateWebsite(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var req models.UpdateWebsiteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIResponse{
|
||||
Code: 400,
|
||||
Message: "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
website, err := h.websiteService.UpdateWebsite(id, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIResponse{
|
||||
Code: 500,
|
||||
Message: "更新失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if website == nil {
|
||||
c.JSON(http.StatusNotFound, models.APIResponse{
|
||||
Code: 404,
|
||||
Message: "网站不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "更新成功",
|
||||
Data: website,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteWebsite 删除网站
|
||||
func (h *WebsiteHandler) DeleteWebsite(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
if err := h.websiteService.DeleteWebsite(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIResponse{
|
||||
Code: 500,
|
||||
Message: "删除失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "删除成功",
|
||||
})
|
||||
}
|
||||
|
||||
// CheckWebsiteNow 立即检测网站
|
||||
func (h *WebsiteHandler) CheckWebsiteNow(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
website := h.websiteService.GetWebsite(id)
|
||||
if website == nil {
|
||||
c.JSON(http.StatusNotFound, models.APIResponse{
|
||||
Code: 404,
|
||||
Message: "网站不存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.monitorService.CheckWebsiteNow(id)
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "检测任务已提交",
|
||||
})
|
||||
}
|
||||
|
||||
// GetGroups 获取所有分组
|
||||
func (h *WebsiteHandler) GetGroups(c *gin.Context) {
|
||||
groups := h.websiteService.GetGroups()
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "success",
|
||||
Data: groups,
|
||||
})
|
||||
}
|
||||
|
||||
// AddGroup 添加分组
|
||||
func (h *WebsiteHandler) AddGroup(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.APIResponse{
|
||||
Code: 400,
|
||||
Message: "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
group, err := h.websiteService.AddGroup(req.Name)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, models.APIResponse{
|
||||
Code: 500,
|
||||
Message: "添加失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: 0,
|
||||
Message: "添加成功",
|
||||
Data: group,
|
||||
})
|
||||
}
|
||||
47
mengyaping-backend/main.go
Normal file
47
mengyaping-backend/main.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"mengyaping-backend/config"
|
||||
"mengyaping-backend/router"
|
||||
"mengyaping-backend/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 获取配置
|
||||
cfg := config.GetConfig()
|
||||
|
||||
// 确保数据目录存在
|
||||
os.MkdirAll(cfg.DataPath, 0755)
|
||||
|
||||
// 启动监控服务
|
||||
monitorService := services.GetMonitorService()
|
||||
go monitorService.Start()
|
||||
|
||||
// 设置路由
|
||||
r := router.SetupRouter()
|
||||
|
||||
// 优雅关闭
|
||||
go func() {
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-sigCh
|
||||
|
||||
log.Println("正在关闭服务...")
|
||||
monitorService.Stop()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// 启动服务器
|
||||
addr := fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
|
||||
log.Printf("🌱 萌芽Ping 监控服务已启动,监听地址: %s\n", addr)
|
||||
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Fatalf("服务器启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
98
mengyaping-backend/models/website.go
Normal file
98
mengyaping-backend/models/website.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Website 网站信息
|
||||
type Website struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"` // 网站名称
|
||||
Group string `json:"group"` // 所属分组
|
||||
URLs []URLInfo `json:"urls"` // 网站访问地址列表
|
||||
Favicon string `json:"favicon"` // 网站图标URL
|
||||
Title string `json:"title"` // 网站标题
|
||||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||||
}
|
||||
|
||||
// URLInfo 单个URL的信息
|
||||
type URLInfo struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"` // 访问地址
|
||||
Remark string `json:"remark"` // 备注说明
|
||||
}
|
||||
|
||||
// MonitorRecord 监控记录
|
||||
type MonitorRecord struct {
|
||||
WebsiteID string `json:"website_id"`
|
||||
URLID string `json:"url_id"`
|
||||
URL string `json:"url"`
|
||||
StatusCode int `json:"status_code"` // HTTP状态码
|
||||
Latency int64 `json:"latency"` // 延迟(毫秒)
|
||||
IsUp bool `json:"is_up"` // 是否可访问
|
||||
Error string `json:"error"` // 错误信息
|
||||
CheckedAt time.Time `json:"checked_at"` // 检测时间
|
||||
}
|
||||
|
||||
// WebsiteStatus 网站状态(用于前端展示)
|
||||
type WebsiteStatus struct {
|
||||
Website Website `json:"website"`
|
||||
URLStatuses []URLStatus `json:"url_statuses"`
|
||||
Uptime24h float64 `json:"uptime_24h"` // 24小时可用率
|
||||
Uptime7d float64 `json:"uptime_7d"` // 7天可用率
|
||||
LastChecked time.Time `json:"last_checked"` // 最后检测时间
|
||||
}
|
||||
|
||||
// URLStatus 单个URL的状态
|
||||
type URLStatus struct {
|
||||
URLInfo URLInfo `json:"url_info"`
|
||||
CurrentState MonitorRecord `json:"current_state"` // 当前状态
|
||||
History24h []MonitorRecord `json:"history_24h"` // 24小时历史
|
||||
History7d []HourlyStats `json:"history_7d"` // 7天按小时统计
|
||||
Uptime24h float64 `json:"uptime_24h"` // 24小时可用率
|
||||
Uptime7d float64 `json:"uptime_7d"` // 7天可用率
|
||||
AvgLatency int64 `json:"avg_latency"` // 平均延迟
|
||||
}
|
||||
|
||||
// HourlyStats 每小时统计
|
||||
type HourlyStats struct {
|
||||
Hour time.Time `json:"hour"`
|
||||
TotalCount int `json:"total_count"`
|
||||
UpCount int `json:"up_count"`
|
||||
AvgLatency int64 `json:"avg_latency"`
|
||||
Uptime float64 `json:"uptime"`
|
||||
}
|
||||
|
||||
// Group 分组
|
||||
type Group struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// DefaultGroups 默认分组
|
||||
var DefaultGroups = []Group{
|
||||
{ID: "normal", Name: "普通网站"},
|
||||
{ID: "admin", Name: "管理员网站"},
|
||||
}
|
||||
|
||||
// CreateWebsiteRequest 创建网站请求
|
||||
type CreateWebsiteRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Group string `json:"group" binding:"required"`
|
||||
URLs []string `json:"urls" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// UpdateWebsiteRequest 更新网站请求
|
||||
type UpdateWebsiteRequest struct {
|
||||
Name string `json:"name"`
|
||||
Group string `json:"group"`
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
// APIResponse API响应
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
51
mengyaping-backend/router/router.go
Normal file
51
mengyaping-backend/router/router.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"mengyaping-backend/handlers"
|
||||
)
|
||||
|
||||
// SetupRouter 设置路由
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
// CORS配置
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
// 创建处理器
|
||||
websiteHandler := handlers.NewWebsiteHandler()
|
||||
|
||||
// API路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 健康检查
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"message": "服务运行正常",
|
||||
})
|
||||
})
|
||||
|
||||
// 网站相关
|
||||
api.GET("/websites", websiteHandler.GetWebsites)
|
||||
api.GET("/websites/:id", websiteHandler.GetWebsite)
|
||||
api.POST("/websites", websiteHandler.CreateWebsite)
|
||||
api.PUT("/websites/:id", websiteHandler.UpdateWebsite)
|
||||
api.DELETE("/websites/:id", websiteHandler.DeleteWebsite)
|
||||
api.POST("/websites/:id/check", websiteHandler.CheckWebsiteNow)
|
||||
|
||||
// 分组相关
|
||||
api.GET("/groups", websiteHandler.GetGroups)
|
||||
api.POST("/groups", websiteHandler.AddGroup)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
302
mengyaping-backend/services/monitor.go
Normal file
302
mengyaping-backend/services/monitor.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/config"
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/storage"
|
||||
"mengyaping-backend/utils"
|
||||
)
|
||||
|
||||
// MonitorService 监控服务
|
||||
type MonitorService struct {
|
||||
httpClient *utils.HTTPClient
|
||||
storage *storage.Storage
|
||||
stopCh chan struct{}
|
||||
running bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
monitorService *MonitorService
|
||||
monitorOnce sync.Once
|
||||
)
|
||||
|
||||
// GetMonitorService 获取监控服务单例
|
||||
func GetMonitorService() *MonitorService {
|
||||
monitorOnce.Do(func() {
|
||||
cfg := config.GetConfig()
|
||||
monitorService = &MonitorService{
|
||||
httpClient: utils.NewHTTPClient(cfg.Monitor.Timeout),
|
||||
storage: storage.GetStorage(),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
})
|
||||
return monitorService
|
||||
}
|
||||
|
||||
// Start 启动监控服务
|
||||
func (s *MonitorService) Start() {
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
s.mu.Unlock()
|
||||
|
||||
log.Println("监控服务已启动")
|
||||
|
||||
// 立即执行一次检测
|
||||
go s.checkAll()
|
||||
|
||||
// 定时检测
|
||||
cfg := config.GetConfig()
|
||||
ticker := time.NewTicker(cfg.Monitor.Interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
go s.checkAll()
|
||||
case <-s.stopCh:
|
||||
log.Println("监控服务已停止")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止监控服务
|
||||
func (s *MonitorService) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.running {
|
||||
close(s.stopCh)
|
||||
s.running = false
|
||||
}
|
||||
}
|
||||
|
||||
// checkAll 检查所有网站
|
||||
func (s *MonitorService) checkAll() {
|
||||
websites := s.storage.GetWebsites()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
semaphore := make(chan struct{}, 10) // 限制并发数
|
||||
|
||||
for _, website := range websites {
|
||||
for _, urlInfo := range website.URLs {
|
||||
wg.Add(1)
|
||||
go func(w models.Website, u models.URLInfo) {
|
||||
defer wg.Done()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
s.checkURL(w, u)
|
||||
}(website, urlInfo)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 保存记录
|
||||
s.storage.SaveAll()
|
||||
}
|
||||
|
||||
// checkURL 检查单个URL
|
||||
func (s *MonitorService) checkURL(website models.Website, urlInfo models.URLInfo) {
|
||||
result := s.httpClient.CheckWebsite(urlInfo.URL)
|
||||
|
||||
record := models.MonitorRecord{
|
||||
WebsiteID: website.ID,
|
||||
URLID: urlInfo.ID,
|
||||
URL: urlInfo.URL,
|
||||
StatusCode: result.StatusCode,
|
||||
Latency: result.Latency.Milliseconds(),
|
||||
IsUp: result.Error == nil && utils.IsSuccessStatus(result.StatusCode),
|
||||
CheckedAt: time.Now(),
|
||||
}
|
||||
|
||||
if result.Error != nil {
|
||||
record.Error = result.Error.Error()
|
||||
}
|
||||
|
||||
s.storage.AddRecord(record)
|
||||
|
||||
// 更新网站信息(标题和Favicon)
|
||||
if result.Title != "" || result.Favicon != "" {
|
||||
w := s.storage.GetWebsite(website.ID)
|
||||
if w != nil {
|
||||
needUpdate := false
|
||||
if result.Title != "" && w.Title != result.Title {
|
||||
w.Title = result.Title
|
||||
needUpdate = true
|
||||
}
|
||||
if result.Favicon != "" && w.Favicon != result.Favicon {
|
||||
w.Favicon = result.Favicon
|
||||
needUpdate = true
|
||||
}
|
||||
if needUpdate {
|
||||
w.UpdatedAt = time.Now()
|
||||
s.storage.UpdateWebsite(*w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("检测 [%s] %s - 状态码: %d, 延迟: %dms, 可用: %v",
|
||||
website.Name, urlInfo.URL, result.StatusCode, result.Latency.Milliseconds(), record.IsUp)
|
||||
}
|
||||
|
||||
// CheckWebsiteNow 立即检查指定网站
|
||||
func (s *MonitorService) CheckWebsiteNow(websiteID string) {
|
||||
website := s.storage.GetWebsite(websiteID)
|
||||
if website == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, urlInfo := range website.URLs {
|
||||
go s.checkURL(*website, urlInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebsiteStatus 获取网站状态
|
||||
func (s *MonitorService) GetWebsiteStatus(websiteID string) *models.WebsiteStatus {
|
||||
website := s.storage.GetWebsite(websiteID)
|
||||
if website == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
status := &models.WebsiteStatus{
|
||||
Website: *website,
|
||||
URLStatuses: []models.URLStatus{},
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
since24h := now.Add(-24 * time.Hour)
|
||||
since7d := now.Add(-7 * 24 * time.Hour)
|
||||
|
||||
var totalUptime24h, totalUptime7d float64
|
||||
var urlCount int
|
||||
|
||||
for _, urlInfo := range website.URLs {
|
||||
urlStatus := s.getURLStatus(website.ID, urlInfo, since24h, since7d)
|
||||
status.URLStatuses = append(status.URLStatuses, urlStatus)
|
||||
|
||||
totalUptime24h += urlStatus.Uptime24h
|
||||
totalUptime7d += urlStatus.Uptime7d
|
||||
urlCount++
|
||||
}
|
||||
|
||||
if urlCount > 0 {
|
||||
status.Uptime24h = totalUptime24h / float64(urlCount)
|
||||
status.Uptime7d = totalUptime7d / float64(urlCount)
|
||||
}
|
||||
|
||||
// 获取最后检测时间
|
||||
for _, urlStatus := range status.URLStatuses {
|
||||
if urlStatus.CurrentState.CheckedAt.After(status.LastChecked) {
|
||||
status.LastChecked = urlStatus.CurrentState.CheckedAt
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
// getURLStatus 获取URL状态
|
||||
func (s *MonitorService) getURLStatus(websiteID string, urlInfo models.URLInfo, since24h, since7d time.Time) models.URLStatus {
|
||||
urlStatus := models.URLStatus{
|
||||
URLInfo: urlInfo,
|
||||
}
|
||||
|
||||
// 获取最新记录
|
||||
latest := s.storage.GetLatestRecord(websiteID, urlInfo.ID)
|
||||
if latest != nil {
|
||||
urlStatus.CurrentState = *latest
|
||||
}
|
||||
|
||||
// 获取24小时记录
|
||||
records24h := s.storage.GetRecords(websiteID, urlInfo.ID, since24h)
|
||||
urlStatus.History24h = records24h
|
||||
|
||||
// 计算24小时可用率
|
||||
if len(records24h) > 0 {
|
||||
upCount := 0
|
||||
var totalLatency int64
|
||||
for _, r := range records24h {
|
||||
if r.IsUp {
|
||||
upCount++
|
||||
}
|
||||
totalLatency += r.Latency
|
||||
}
|
||||
urlStatus.Uptime24h = float64(upCount) / float64(len(records24h)) * 100
|
||||
urlStatus.AvgLatency = totalLatency / int64(len(records24h))
|
||||
}
|
||||
|
||||
// 获取7天记录并按小时统计
|
||||
records7d := s.storage.GetRecords(websiteID, urlInfo.ID, since7d)
|
||||
urlStatus.History7d = s.aggregateByHour(records7d)
|
||||
|
||||
// 计算7天可用率
|
||||
if len(records7d) > 0 {
|
||||
upCount := 0
|
||||
for _, r := range records7d {
|
||||
if r.IsUp {
|
||||
upCount++
|
||||
}
|
||||
}
|
||||
urlStatus.Uptime7d = float64(upCount) / float64(len(records7d)) * 100
|
||||
}
|
||||
|
||||
return urlStatus
|
||||
}
|
||||
|
||||
// aggregateByHour 按小时聚合记录
|
||||
func (s *MonitorService) aggregateByHour(records []models.MonitorRecord) []models.HourlyStats {
|
||||
hourlyMap := make(map[string]*models.HourlyStats)
|
||||
|
||||
for _, r := range records {
|
||||
hourKey := r.CheckedAt.Truncate(time.Hour).Format(time.RFC3339)
|
||||
|
||||
if _, exists := hourlyMap[hourKey]; !exists {
|
||||
hourlyMap[hourKey] = &models.HourlyStats{
|
||||
Hour: r.CheckedAt.Truncate(time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
stats := hourlyMap[hourKey]
|
||||
stats.TotalCount++
|
||||
if r.IsUp {
|
||||
stats.UpCount++
|
||||
}
|
||||
stats.AvgLatency += r.Latency
|
||||
}
|
||||
|
||||
var result []models.HourlyStats
|
||||
for _, stats := range hourlyMap {
|
||||
if stats.TotalCount > 0 {
|
||||
stats.AvgLatency /= int64(stats.TotalCount)
|
||||
stats.Uptime = float64(stats.UpCount) / float64(stats.TotalCount) * 100
|
||||
}
|
||||
result = append(result, *stats)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetAllWebsiteStatuses 获取所有网站状态
|
||||
func (s *MonitorService) GetAllWebsiteStatuses() []models.WebsiteStatus {
|
||||
websites := s.storage.GetWebsites()
|
||||
var statuses []models.WebsiteStatus
|
||||
|
||||
for _, website := range websites {
|
||||
status := s.GetWebsiteStatus(website.ID)
|
||||
if status != nil {
|
||||
statuses = append(statuses, *status)
|
||||
}
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
127
mengyaping-backend/services/website.go
Normal file
127
mengyaping-backend/services/website.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/models"
|
||||
"mengyaping-backend/storage"
|
||||
"mengyaping-backend/utils"
|
||||
)
|
||||
|
||||
// WebsiteService 网站服务
|
||||
type WebsiteService struct {
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewWebsiteService 创建网站服务
|
||||
func NewWebsiteService() *WebsiteService {
|
||||
return &WebsiteService{
|
||||
storage: storage.GetStorage(),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWebsite 创建网站
|
||||
func (s *WebsiteService) CreateWebsite(req models.CreateWebsiteRequest) (*models.Website, error) {
|
||||
website := models.Website{
|
||||
ID: utils.GenerateID(),
|
||||
Name: req.Name,
|
||||
Group: req.Group,
|
||||
URLs: make([]models.URLInfo, 0),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
for _, url := range req.URLs {
|
||||
urlInfo := models.URLInfo{
|
||||
ID: utils.GenerateShortID(),
|
||||
URL: url,
|
||||
}
|
||||
website.URLs = append(website.URLs, urlInfo)
|
||||
}
|
||||
|
||||
if err := s.storage.AddWebsite(website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 立即检测该网站
|
||||
go GetMonitorService().CheckWebsiteNow(website.ID)
|
||||
|
||||
return &website, nil
|
||||
}
|
||||
|
||||
// GetWebsite 获取网站
|
||||
func (s *WebsiteService) GetWebsite(id string) *models.Website {
|
||||
return s.storage.GetWebsite(id)
|
||||
}
|
||||
|
||||
// GetAllWebsites 获取所有网站
|
||||
func (s *WebsiteService) GetAllWebsites() []models.Website {
|
||||
return s.storage.GetWebsites()
|
||||
}
|
||||
|
||||
// UpdateWebsite 更新网站
|
||||
func (s *WebsiteService) UpdateWebsite(id string, req models.UpdateWebsiteRequest) (*models.Website, error) {
|
||||
website := s.storage.GetWebsite(id)
|
||||
if website == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
website.Name = req.Name
|
||||
}
|
||||
if req.Group != "" {
|
||||
website.Group = req.Group
|
||||
}
|
||||
if len(req.URLs) > 0 {
|
||||
// 保留已有URL的ID,添加新URL
|
||||
existingURLs := make(map[string]models.URLInfo)
|
||||
for _, u := range website.URLs {
|
||||
existingURLs[u.URL] = u
|
||||
}
|
||||
|
||||
newURLs := make([]models.URLInfo, 0)
|
||||
for _, url := range req.URLs {
|
||||
if existing, ok := existingURLs[url]; ok {
|
||||
newURLs = append(newURLs, existing)
|
||||
} else {
|
||||
newURLs = append(newURLs, models.URLInfo{
|
||||
ID: utils.GenerateShortID(),
|
||||
URL: url,
|
||||
})
|
||||
}
|
||||
}
|
||||
website.URLs = newURLs
|
||||
}
|
||||
|
||||
website.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.storage.UpdateWebsite(*website); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return website, nil
|
||||
}
|
||||
|
||||
// DeleteWebsite 删除网站
|
||||
func (s *WebsiteService) DeleteWebsite(id string) error {
|
||||
return s.storage.DeleteWebsite(id)
|
||||
}
|
||||
|
||||
// GetGroups 获取所有分组
|
||||
func (s *WebsiteService) GetGroups() []models.Group {
|
||||
return s.storage.GetGroups()
|
||||
}
|
||||
|
||||
// AddGroup 添加分组
|
||||
func (s *WebsiteService) AddGroup(name string) (*models.Group, error) {
|
||||
group := models.Group{
|
||||
ID: utils.GenerateShortID(),
|
||||
Name: name,
|
||||
}
|
||||
|
||||
if err := s.storage.AddGroup(group); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &group, nil
|
||||
}
|
||||
285
mengyaping-backend/storage/storage.go
Normal file
285
mengyaping-backend/storage/storage.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"mengyaping-backend/config"
|
||||
"mengyaping-backend/models"
|
||||
)
|
||||
|
||||
// Storage 数据存储
|
||||
type Storage struct {
|
||||
dataPath string
|
||||
mu sync.RWMutex
|
||||
websites []models.Website
|
||||
records map[string][]models.MonitorRecord // key: websiteID_urlID
|
||||
groups []models.Group
|
||||
}
|
||||
|
||||
var (
|
||||
store *Storage
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetStorage 获取存储单例
|
||||
func GetStorage() *Storage {
|
||||
once.Do(func() {
|
||||
cfg := config.GetConfig()
|
||||
store = &Storage{
|
||||
dataPath: cfg.DataPath,
|
||||
websites: []models.Website{},
|
||||
records: make(map[string][]models.MonitorRecord),
|
||||
groups: models.DefaultGroups,
|
||||
}
|
||||
store.ensureDataDir()
|
||||
store.load()
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
// ensureDataDir 确保数据目录存在
|
||||
func (s *Storage) ensureDataDir() {
|
||||
os.MkdirAll(s.dataPath, 0755)
|
||||
}
|
||||
|
||||
// load 加载数据
|
||||
func (s *Storage) load() {
|
||||
s.loadWebsites()
|
||||
s.loadRecords()
|
||||
s.loadGroups()
|
||||
}
|
||||
|
||||
// loadWebsites 加载网站数据
|
||||
func (s *Storage) loadWebsites() {
|
||||
filePath := filepath.Join(s.dataPath, "websites.json")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &s.websites)
|
||||
}
|
||||
|
||||
// loadRecords 加载监控记录
|
||||
func (s *Storage) loadRecords() {
|
||||
filePath := filepath.Join(s.dataPath, "records.json")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &s.records)
|
||||
|
||||
// 清理过期记录
|
||||
s.cleanOldRecords()
|
||||
}
|
||||
|
||||
// loadGroups 加载分组
|
||||
func (s *Storage) loadGroups() {
|
||||
filePath := filepath.Join(s.dataPath, "groups.json")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
// 使用默认分组
|
||||
s.groups = models.DefaultGroups
|
||||
s.saveGroups()
|
||||
return
|
||||
}
|
||||
json.Unmarshal(data, &s.groups)
|
||||
}
|
||||
|
||||
// saveWebsites 保存网站数据
|
||||
func (s *Storage) saveWebsites() error {
|
||||
filePath := filepath.Join(s.dataPath, "websites.json")
|
||||
data, err := json.MarshalIndent(s.websites, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
||||
// saveRecords 保存监控记录
|
||||
func (s *Storage) saveRecords() error {
|
||||
filePath := filepath.Join(s.dataPath, "records.json")
|
||||
data, err := json.MarshalIndent(s.records, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
||||
// saveGroups 保存分组
|
||||
func (s *Storage) saveGroups() error {
|
||||
filePath := filepath.Join(s.dataPath, "groups.json")
|
||||
data, err := json.MarshalIndent(s.groups, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filePath, data, 0644)
|
||||
}
|
||||
|
||||
// cleanOldRecords 清理过期记录
|
||||
func (s *Storage) cleanOldRecords() {
|
||||
cfg := config.GetConfig()
|
||||
cutoff := time.Now().AddDate(0, 0, -cfg.Monitor.HistoryDays)
|
||||
|
||||
for key, records := range s.records {
|
||||
var newRecords []models.MonitorRecord
|
||||
for _, r := range records {
|
||||
if r.CheckedAt.After(cutoff) {
|
||||
newRecords = append(newRecords, r)
|
||||
}
|
||||
}
|
||||
s.records[key] = newRecords
|
||||
}
|
||||
}
|
||||
|
||||
// GetWebsites 获取所有网站
|
||||
func (s *Storage) GetWebsites() []models.Website {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]models.Website, len(s.websites))
|
||||
copy(result, s.websites)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetWebsite 获取单个网站
|
||||
func (s *Storage) GetWebsite(id string) *models.Website {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for _, w := range s.websites {
|
||||
if w.ID == id {
|
||||
website := w
|
||||
return &website
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddWebsite 添加网站
|
||||
func (s *Storage) AddWebsite(website models.Website) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.websites = append(s.websites, website)
|
||||
return s.saveWebsites()
|
||||
}
|
||||
|
||||
// UpdateWebsite 更新网站
|
||||
func (s *Storage) UpdateWebsite(website models.Website) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, w := range s.websites {
|
||||
if w.ID == website.ID {
|
||||
s.websites[i] = website
|
||||
return s.saveWebsites()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWebsite 删除网站
|
||||
func (s *Storage) DeleteWebsite(id string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, w := range s.websites {
|
||||
if w.ID == id {
|
||||
s.websites = append(s.websites[:i], s.websites[i+1:]...)
|
||||
// 删除相关记录
|
||||
for key := range s.records {
|
||||
if len(key) > len(id) && key[:len(id)] == id {
|
||||
delete(s.records, key)
|
||||
}
|
||||
}
|
||||
s.saveRecords()
|
||||
return s.saveWebsites()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddRecord 添加监控记录
|
||||
func (s *Storage) AddRecord(record models.MonitorRecord) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := record.WebsiteID + "_" + record.URLID
|
||||
s.records[key] = append(s.records[key], record)
|
||||
|
||||
// 每100条记录保存一次
|
||||
if len(s.records[key])%100 == 0 {
|
||||
return s.saveRecords()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRecords 获取监控记录
|
||||
func (s *Storage) GetRecords(websiteID, urlID string, since time.Time) []models.MonitorRecord {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
key := websiteID + "_" + urlID
|
||||
records := s.records[key]
|
||||
|
||||
var result []models.MonitorRecord
|
||||
for _, r := range records {
|
||||
if r.CheckedAt.After(since) {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetLatestRecord 获取最新记录
|
||||
func (s *Storage) GetLatestRecord(websiteID, urlID string) *models.MonitorRecord {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
key := websiteID + "_" + urlID
|
||||
records := s.records[key]
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
latest := records[len(records)-1]
|
||||
return &latest
|
||||
}
|
||||
|
||||
// GetGroups 获取所有分组
|
||||
func (s *Storage) GetGroups() []models.Group {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
result := make([]models.Group, len(s.groups))
|
||||
copy(result, s.groups)
|
||||
return result
|
||||
}
|
||||
|
||||
// AddGroup 添加分组
|
||||
func (s *Storage) AddGroup(group models.Group) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.groups = append(s.groups, group)
|
||||
return s.saveGroups()
|
||||
}
|
||||
|
||||
// SaveAll 保存所有数据
|
||||
func (s *Storage) SaveAll() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := s.saveWebsites(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.saveRecords(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.saveGroups()
|
||||
}
|
||||
131
mengyaping-backend/utils/http.go
Normal file
131
mengyaping-backend/utils/http.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPClient HTTP客户端工具
|
||||
type HTTPClient struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewHTTPClient 创建HTTP客户端
|
||||
func NewHTTPClient(timeout time.Duration) *HTTPClient {
|
||||
return &HTTPClient{
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("too many redirects")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CheckResult 检查结果
|
||||
type CheckResult struct {
|
||||
StatusCode int
|
||||
Latency time.Duration
|
||||
Title string
|
||||
Favicon string
|
||||
Error error
|
||||
}
|
||||
|
||||
// CheckWebsite 检查网站
|
||||
func (c *HTTPClient) CheckWebsite(targetURL string) CheckResult {
|
||||
result := CheckResult{}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
req, err := http.NewRequest("GET", targetURL, nil)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
return result
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "MengYaPing/1.0 (Website Monitor)")
|
||||
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
result.Latency = time.Since(start)
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result.Latency = time.Since(start)
|
||||
result.StatusCode = resp.StatusCode
|
||||
|
||||
// 读取响应体获取标题
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100)) // 限制100KB
|
||||
if err == nil {
|
||||
result.Title = extractTitle(string(body))
|
||||
result.Favicon = extractFavicon(string(body), targetURL)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// extractTitle 提取网页标题
|
||||
func extractTitle(html string) string {
|
||||
re := regexp.MustCompile(`(?i)<title[^>]*>([^<]+)</title>`)
|
||||
matches := re.FindStringSubmatch(html)
|
||||
if len(matches) > 1 {
|
||||
return strings.TrimSpace(matches[1])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractFavicon 提取Favicon
|
||||
func extractFavicon(html string, baseURL string) string {
|
||||
parsedURL, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 尝试从HTML中提取favicon链接
|
||||
patterns := []string{
|
||||
`(?i)<link[^>]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']+)["']`,
|
||||
`(?i)<link[^>]*href=["']([^"']+)["'][^>]*rel=["'](?:shortcut )?icon["']`,
|
||||
`(?i)<link[^>]*rel=["']apple-touch-icon["'][^>]*href=["']([^"']+)["']`,
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
re := regexp.MustCompile(pattern)
|
||||
matches := re.FindStringSubmatch(html)
|
||||
if len(matches) > 1 {
|
||||
faviconURL := matches[1]
|
||||
return resolveURL(parsedURL, faviconURL)
|
||||
}
|
||||
}
|
||||
|
||||
// 默认返回 /favicon.ico
|
||||
return fmt.Sprintf("%s://%s/favicon.ico", parsedURL.Scheme, parsedURL.Host)
|
||||
}
|
||||
|
||||
// resolveURL 解析相对URL
|
||||
func resolveURL(base *url.URL, ref string) string {
|
||||
refURL, err := url.Parse(ref)
|
||||
if err != nil {
|
||||
return ref
|
||||
}
|
||||
return base.ResolveReference(refURL).String()
|
||||
}
|
||||
|
||||
// IsSuccessStatus 判断是否为成功状态码
|
||||
func IsSuccessStatus(statusCode int) bool {
|
||||
return statusCode >= 200 && statusCode < 400
|
||||
}
|
||||
31
mengyaping-backend/utils/id.go
Normal file
31
mengyaping-backend/utils/id.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateID 生成唯一ID
|
||||
func GenerateID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
randomBytes := make([]byte, 4)
|
||||
rand.Read(randomBytes)
|
||||
return hex.EncodeToString([]byte{
|
||||
byte(timestamp >> 56),
|
||||
byte(timestamp >> 48),
|
||||
byte(timestamp >> 40),
|
||||
byte(timestamp >> 32),
|
||||
byte(timestamp >> 24),
|
||||
byte(timestamp >> 16),
|
||||
byte(timestamp >> 8),
|
||||
byte(timestamp),
|
||||
}) + hex.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
// GenerateShortID 生成短ID
|
||||
func GenerateShortID() string {
|
||||
randomBytes := make([]byte, 6)
|
||||
rand.Read(randomBytes)
|
||||
return hex.EncodeToString(randomBytes)
|
||||
}
|
||||
Reference in New Issue
Block a user