mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 11:29:37 +08:00
Compare commits
65 Commits
v1.3.0
...
feat_plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b70e4eff95 | ||
|
|
776de0bcc0 | ||
|
|
9efe50883d | ||
|
|
207eb714da | ||
|
|
7fd33cdcd1 | ||
|
|
0806ef7a69 | ||
|
|
fdb8e8a484 | ||
|
|
2638ccb1e4 | ||
|
|
886d91ab10 | ||
|
|
ddad95be41 | ||
|
|
273800459f | ||
|
|
dbe24af4ac | ||
|
|
a598ef508c | ||
|
|
1ca4cce6bc | ||
|
|
270022188e | ||
|
|
7e80a1c2b2 | ||
|
|
6e7914f056 | ||
|
|
dbde0e1675 | ||
|
|
b840680df0 | ||
|
|
651987731b | ||
|
|
fb26d166d6 | ||
|
|
8baf5c6c3d | ||
|
|
005aa71cc2 | ||
|
|
61beed6788 | ||
|
|
53aebf2a15 | ||
|
|
1fe9487833 | ||
|
|
6476ce1369 | ||
|
|
1ad3a07930 | ||
|
|
22fd1dcf81 | ||
|
|
f8cfe307ae | ||
|
|
84ee0d9e53 | ||
|
|
40e3350a4b | ||
|
|
013fe71925 | ||
|
|
6be7ae871d | ||
|
|
89e2aca968 | ||
|
|
f006d84b03 | ||
|
|
7ce3839b9b | ||
|
|
52ea019374 | ||
|
|
4c738d1030 | ||
|
|
ec00f2d823 | ||
|
|
54542ff8ee | ||
|
|
0050c6bba3 | ||
|
|
4ceed8fd4b | ||
|
|
2e5dd8360e | ||
|
|
40ad48f5cf | ||
|
|
921bdc43cb | ||
|
|
0df7d8bf23 | ||
|
|
fdc75705aa | ||
|
|
a28dd4840b | ||
|
|
061b94cf61 | ||
|
|
0d28b322b7 | ||
|
|
ee06e110bd | ||
|
|
7acfa300ea | ||
|
|
b4689d2f99 | ||
|
|
6074d91467 | ||
|
|
e30e381adf | ||
|
|
516746f722 | ||
|
|
4da07b3ea4 | ||
|
|
da8a2ad169 | ||
|
|
e2832b9e36 | ||
|
|
bdb43531e8 | ||
|
|
51dbf0f03a | ||
|
|
10294e093f | ||
|
|
6816ab0550 | ||
|
|
800b511116 |
31
.claude/settings.local.json
Normal file
31
.claude/settings.local.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(findstr:*)",
|
||||
"Bash(go get:*)",
|
||||
"Bash(go run:*)",
|
||||
"Bash(Get-NetTCPConnection:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(timeout 5 curl:*)",
|
||||
"Bash(npm run build:*)",
|
||||
"Bash(go build:*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(del:*)",
|
||||
"Bash(STRUCTURED_LOG=true go run:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(git restore:*)",
|
||||
"mcp__context7__resolve-library-id",
|
||||
"mcp__context7__get-library-docs",
|
||||
"Bash(npm run dev)",
|
||||
"Read(//g/server/**)",
|
||||
"mcp__playwright__browser_navigate",
|
||||
"mcp__playwright__browser_snapshot",
|
||||
"mcp__playwright__browser_console_messages",
|
||||
"Bash(move \"G:\\server\\urldb\\plugin\\registry\\registry.go\" \"G:\\server\\urldb\\plugin\\registry\\registry.go\")",
|
||||
"Bash(go test:*)",
|
||||
"Bash(./main:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -1,5 +1,9 @@
|
||||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
id-token: write
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -55,4 +59,4 @@ jobs:
|
||||
files: |
|
||||
urldb-${{ github.ref_name }}-linux-amd64
|
||||
frontend-${{ github.ref_name }}.tar.gz
|
||||
generate_release_notes: true
|
||||
generate_release_notes: true
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -123,4 +123,8 @@ dist/
|
||||
.dockerignore
|
||||
|
||||
# Air live reload
|
||||
tmp/
|
||||
tmp/
|
||||
|
||||
# plugin
|
||||
plugins/
|
||||
data/
|
||||
@@ -56,7 +56,7 @@ cp env.example .env
|
||||
vim .env
|
||||
|
||||
# 启动开发服务器
|
||||
go run main.go
|
||||
go run .
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
|
||||
25
ChangeLog.md
25
ChangeLog.md
@@ -1,10 +1,21 @@
|
||||
### v1.2.6
|
||||
1. 支持迅雷云盘
|
||||
2. 优化热播剧采集和页面显示
|
||||
3. 首页添加标签显示
|
||||
4. 后端 UI 优化
|
||||
5. 新增 Telegram Bot
|
||||
6. 新增扩容
|
||||
### v1.3.3
|
||||
1. 公众号自动回复
|
||||
|
||||
### v1.3.2
|
||||
1. 二维码美化
|
||||
2. TelegramBot参数调整
|
||||
3. 修复一些问题
|
||||
|
||||
### v1.3.1
|
||||
1. 添加API访问日志
|
||||
2. 添加首页公告
|
||||
3. TG机器人,添加资源选择模式
|
||||
|
||||
### v1.3.0
|
||||
1. 新增 Telegram Bot
|
||||
2. 新增扩容
|
||||
3. 支持迅雷云盘
|
||||
4. UI优化
|
||||
|
||||
### v1.2.5
|
||||
1. 修复一些Bug
|
||||
|
||||
262
PLUGIN_TESTING.md
Normal file
262
PLUGIN_TESTING.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# urlDB Plugin Test Framework
|
||||
|
||||
This document describes the plugin test framework for urlDB.
|
||||
|
||||
## Overview
|
||||
|
||||
The plugin test framework provides a comprehensive set of tools for testing urlDB plugins. It includes:
|
||||
|
||||
1. **Unit Testing Framework** - For testing individual plugin components
|
||||
2. **Integration Testing Environment** - For testing plugins in a complete system environment
|
||||
3. **Test Reporting** - For generating detailed test reports
|
||||
4. **Mock Objects** - For simulating system components during testing
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Unit Testing Framework (`plugin/test/framework.go`)
|
||||
|
||||
The unit testing framework provides:
|
||||
|
||||
- `TestPluginContext` - A mock implementation of the PluginContext interface for testing plugin interactions with the system
|
||||
- `TestPluginManager` - A test helper for managing plugin lifecycle in tests
|
||||
- Logging and assertion utilities
|
||||
- Configuration and data storage simulation
|
||||
- Task scheduling simulation
|
||||
- Cache system simulation
|
||||
- Security permissions simulation
|
||||
- Concurrency control simulation
|
||||
|
||||
### 2. Integration Testing Environment (`plugin/test/integration.go`)
|
||||
|
||||
The integration testing environment provides:
|
||||
|
||||
- `IntegrationTestSuite` - A complete integration test suite with database, repository manager, task manager, etc.
|
||||
- `MockPlugin` - A mock plugin implementation for testing plugin manager functionality
|
||||
- Various error scenario mock plugins
|
||||
- Dependency relationship simulation
|
||||
- Context operation simulation
|
||||
|
||||
### 3. Test Reporting (`plugin/test/reporting.go`)
|
||||
|
||||
The test reporting system provides:
|
||||
|
||||
- `TestReport` - Test report structure
|
||||
- `TestReporter` - Test report generator
|
||||
- `TestingTWrapper` - Wrapper for Go testing framework integration
|
||||
- `PluginTestHelper` - Plugin test helper with specialized plugin testing functions
|
||||
|
||||
## Usage
|
||||
|
||||
### Writing Unit Tests
|
||||
|
||||
To write unit tests for plugins, follow this example:
|
||||
|
||||
```go
|
||||
func TestMyPlugin(t *testing.T) {
|
||||
plugin := NewMyPlugin()
|
||||
|
||||
// Create test context
|
||||
ctx := test.NewTestPluginContext()
|
||||
|
||||
// Initialize plugin
|
||||
if err := plugin.Initialize(ctx); err != nil {
|
||||
t.Fatalf("Failed to initialize plugin: %v", err)
|
||||
}
|
||||
|
||||
// Verify initialization logs
|
||||
if !ctx.AssertLogContains(t, "INFO", "Plugin initialized") {
|
||||
t.Error("Expected initialization log")
|
||||
}
|
||||
|
||||
// Test other functionality...
|
||||
}
|
||||
```
|
||||
|
||||
### Writing Integration Tests
|
||||
|
||||
To write integration tests, follow this example:
|
||||
|
||||
```go
|
||||
func TestMyPluginIntegration(t *testing.T) {
|
||||
// Create integration test suite
|
||||
suite := test.NewIntegrationTestSuite()
|
||||
suite.Setup(t)
|
||||
defer suite.Teardown()
|
||||
|
||||
// Register plugin
|
||||
plugin := NewMyPlugin()
|
||||
if err := suite.RegisterPlugin(plugin); err != nil {
|
||||
t.Fatalf("Failed to register plugin: %v", err)
|
||||
}
|
||||
|
||||
// Run integration test
|
||||
config := map[string]interface{}{
|
||||
"setting1": "value1",
|
||||
}
|
||||
|
||||
suite.RunPluginIntegrationTest(t, plugin.Name(), config)
|
||||
}
|
||||
```
|
||||
|
||||
### Generating Test Reports
|
||||
|
||||
Test reports are automatically generated, but you can also create them manually:
|
||||
|
||||
```go
|
||||
func TestWithReporting(t *testing.T) {
|
||||
// Create reporter
|
||||
reporter := test.NewTestReporter("MyTestSuite")
|
||||
wrapper := test.NewTestingTWrapper(t, reporter)
|
||||
|
||||
// Use wrapper to run tests
|
||||
wrapper.Run("MyTest", func(t *testing.T) {
|
||||
// Test code...
|
||||
})
|
||||
|
||||
// Generate report
|
||||
textReport := reporter.GenerateTextReport()
|
||||
t.Logf("Test Report:\n%s", textReport)
|
||||
}
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run All Plugin Tests
|
||||
|
||||
```bash
|
||||
go test ./plugin/...
|
||||
```
|
||||
|
||||
### Run Specific Tests
|
||||
|
||||
```bash
|
||||
go test ./plugin/demo/ -v
|
||||
```
|
||||
|
||||
### Generate Test Coverage Report
|
||||
|
||||
```bash
|
||||
go test ./plugin/... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Test Plugin Lifecycle
|
||||
|
||||
Ensure you test the complete plugin lifecycle:
|
||||
|
||||
```go
|
||||
func TestPluginLifecycle(t *testing.T) {
|
||||
manager := test.NewTestPluginManager()
|
||||
plugin := NewMyPlugin()
|
||||
|
||||
// Register plugin
|
||||
manager.RegisterPlugin(plugin)
|
||||
|
||||
// Test complete lifecycle
|
||||
config := map[string]interface{}{
|
||||
"config_key": "config_value",
|
||||
}
|
||||
|
||||
if err := manager.RunPluginLifecycle(t, plugin.Name(), config); err != nil {
|
||||
t.Errorf("Plugin lifecycle failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Test Error Handling
|
||||
|
||||
Ensure you test plugin behavior under various error conditions:
|
||||
|
||||
```go
|
||||
func TestPluginErrorHandling(t *testing.T) {
|
||||
// Test initialization error
|
||||
pluginWithInitError := test.NewIntegrationTestSuite().
|
||||
CreateMockPlugin("error-plugin", "1.0.0").
|
||||
WithErrorOnInitialize()
|
||||
|
||||
ctx := test.NewTestPluginContext()
|
||||
if err := pluginWithInitError.Initialize(ctx); err == nil {
|
||||
t.Error("Expected initialize error")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Test Dependencies
|
||||
|
||||
Test plugin dependency handling:
|
||||
|
||||
```go
|
||||
func TestPluginDependencies(t *testing.T) {
|
||||
plugin := test.NewIntegrationTestSuite().
|
||||
CreateMockPlugin("dep-plugin", "1.0.0").
|
||||
WithDependencies([]string{"dep1", "dep2"})
|
||||
|
||||
deps := plugin.Dependencies()
|
||||
if len(deps) != 2 {
|
||||
t.Errorf("Expected 2 dependencies, got %d", len(deps))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Test Context Operations
|
||||
|
||||
Test plugin interactions with the system context:
|
||||
|
||||
```go
|
||||
func TestPluginContextOperations(t *testing.T) {
|
||||
operations := []string{
|
||||
"log_info",
|
||||
"set_config",
|
||||
"get_data",
|
||||
}
|
||||
|
||||
plugin := test.NewIntegrationTestSuite().
|
||||
CreateMockPlugin("context-plugin", "1.0.0").
|
||||
WithContextOperations(operations)
|
||||
|
||||
ctx := test.NewTestPluginContext()
|
||||
plugin.Initialize(ctx)
|
||||
plugin.Start()
|
||||
|
||||
// Verify operation results
|
||||
if !ctx.AssertLogContains(t, "INFO", "Info message") {
|
||||
t.Error("Expected info log")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Extending the Framework
|
||||
|
||||
### Adding New Test Features
|
||||
|
||||
To extend the test framework, you can:
|
||||
|
||||
1. Add new mock methods to `TestPluginContext`
|
||||
2. Add new test helper methods to `TestPluginManager`
|
||||
3. Add new reporting features to `TestReporter`
|
||||
|
||||
### Custom Report Formats
|
||||
|
||||
To create custom report formats, you can:
|
||||
|
||||
1. Extend the `TestReport` structure
|
||||
2. Create new report generation methods
|
||||
3. Implement specific report output formats
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Tests fail but no error message**
|
||||
- Check if test assertions are used correctly
|
||||
- Ensure test context is configured correctly
|
||||
|
||||
2. **Integration test environment setup fails**
|
||||
- Check database connection configuration
|
||||
- Ensure all dependent services are available
|
||||
|
||||
3. **Test reports are incomplete**
|
||||
- Ensure test reporter is used correctly
|
||||
- Check if tests complete normally
|
||||
39
README.md
39
README.md
@@ -10,6 +10,10 @@
|
||||
|
||||
**一个现代化的网盘资源数据库,支持多网盘自动化转存分享,支持百度网盘,阿里云盘,夸克网盘, 天翼云盘,迅雷云盘,123云盘,115网盘,UC网盘 **
|
||||
|
||||
免费电报资源频道: [@xypan](https://t.me/xypan) 自动推送资源
|
||||
|
||||
免费电报资源机器人: [@L9ResBot](https://t.me/L9ResBot) 发送 搜索 + 名字 可搜索资源
|
||||
|
||||
🌐 [在线演示](https://pan.l9.lc) | 📖 [文档](https://ecn5khs4t956.feishu.cn/wiki/PsnDwtxghiP0mLkTiruczKtxnwd?from=from_copylink) | 🐛 [问题反馈](https://github.com/ctwj/urldb/issues) | ⭐ [给个星标](https://github.com/ctwj/urldb)
|
||||
|
||||
### 支持的网盘平台
|
||||
@@ -34,23 +38,20 @@
|
||||
- [文档说明](https://ecn5khs4t956.feishu.cn/wiki/PsnDwtxghiP0mLkTiruczKtxnwd?from=from_copylink)
|
||||
- [服务器要求](https://ecn5khs4t956.feishu.cn/wiki/W8YBww1Mmiu4Cdkp5W4c8pFNnMf?from=from_copylink)
|
||||
- [QQ机器人](https://github.com/ctwj/astrbot_plugin_urldb)
|
||||
- [Telegram机器人](https://ecn5khs4t956.feishu.cn/wiki/SwkQw6AzRiFes7kxJXac3pd2ncb?from=from_copylink)
|
||||
|
||||
### v1.2.5
|
||||
1. 修复一些Bug
|
||||
### v1.3.3
|
||||
1. 新增公众号自动回复
|
||||
2. 修复一些问题
|
||||
|
||||
### v1.2.4
|
||||
|
||||
1. 搜索增强,毫秒级响应,关键字高亮显示
|
||||
2. 修复版本显示不正确的问题
|
||||
3. 配置项新增Meilisearch配置
|
||||
|
||||
[详细改动记录](https://github.com/ctwj/urldb/blob/main/ChangeLog.md)
|
||||
|
||||
当前特性
|
||||
1. 支持API,手动批量录入资源
|
||||
2. 支持,自动判断资源有效性
|
||||
3. 支持自动转存(Quark)
|
||||
4. 支持平台多账号管理(Quark)
|
||||
3. 支持自动转存
|
||||
4. 支持平台多账号管理
|
||||
5. 支持简单的数据统计
|
||||
6. 支持Meilisearch
|
||||
|
||||
@@ -121,17 +122,12 @@ PORT=8080
|
||||
|
||||
# 时区配置
|
||||
TIMEZONE=Asia/Shanghai
|
||||
```
|
||||
|
||||
### 镜像构建
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO # 日志级别 (DEBUG, INFO, WARN, ERROR, FATAL)
|
||||
DEBUG=false # 调试模式开关
|
||||
STRUCTURED_LOG=false # 结构化日志开关 (JSON格式)
|
||||
```
|
||||
docker build -t ctwj/urldb-frontend:1.0.7 --target frontend .
|
||||
docker build -t ctwj/urldb-backend:1.0.7 --target backend .
|
||||
docker push ctwj/urldb-frontend:1.0.7
|
||||
docker push ctwj/urldb-backend:1.0.7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 许可证
|
||||
@@ -151,11 +147,8 @@ docker push ctwj/urldb-backend:1.0.7
|
||||
|
||||
---
|
||||
|
||||
## 📞 联系我们
|
||||
|
||||
- **项目地址**: [https://github.com/ctwj/urldb](https://github.com/ctwj/urldb)
|
||||
- **问题反馈**: [Issues](https://github.com/ctwj/urldb/issues)
|
||||
- **邮箱**: 510199617@qq.com
|
||||
## 📞 交流群
|
||||
- **TG**: [Telegram 技术交流群](https://t.me/+QF9OMpOv-PBjZGEx)
|
||||
|
||||
---
|
||||
|
||||
|
||||
79
builtin_plugin.go
Normal file
79
builtin_plugin.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// BuiltinPlugin 内置插件用于测试
|
||||
type BuiltinPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
}
|
||||
|
||||
// NewBuiltinPlugin 创建内置插件实例
|
||||
func NewBuiltinPlugin() *BuiltinPlugin {
|
||||
return &BuiltinPlugin{
|
||||
name: "builtin-demo",
|
||||
version: "1.0.0",
|
||||
description: "内置演示插件,用于测试插件系统功能",
|
||||
author: "urlDB Team",
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *BuiltinPlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Version 返回插件版本
|
||||
func (p *BuiltinPlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
// Description 返回插件描述
|
||||
func (p *BuiltinPlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// Author 返回插件作者
|
||||
func (p *BuiltinPlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *BuiltinPlugin) Initialize(ctx types.PluginContext) error {
|
||||
utils.Info("Initializing builtin plugin: %s", p.name)
|
||||
ctx.LogInfo("Builtin plugin %s initialized successfully", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动插件
|
||||
func (p *BuiltinPlugin) Start() error {
|
||||
utils.Info("Starting builtin plugin: %s", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止插件
|
||||
func (p *BuiltinPlugin) Stop() error {
|
||||
utils.Info("Stopping builtin plugin: %s", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup 清理插件资源
|
||||
func (p *BuiltinPlugin) Cleanup() error {
|
||||
utils.Info("Cleaning up builtin plugin: %s", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dependencies 返回插件依赖
|
||||
func (p *BuiltinPlugin) Dependencies() []string {
|
||||
return []string{} // 无依赖
|
||||
}
|
||||
|
||||
// CheckDependencies 检查插件依赖
|
||||
func (p *BuiltinPlugin) CheckDependencies() map[string]bool {
|
||||
return map[string]bool{} // 无依赖需要检查
|
||||
}
|
||||
676
config/config.go
Normal file
676
config/config.go
Normal file
@@ -0,0 +1,676 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// ConfigManager 统一配置管理器
|
||||
type ConfigManager struct {
|
||||
repo *repo.RepositoryManager
|
||||
|
||||
// 内存缓存
|
||||
cache map[string]*ConfigItem
|
||||
cacheMutex sync.RWMutex
|
||||
cacheOnce sync.Once
|
||||
|
||||
// 配置更新通知
|
||||
configUpdateCh chan string
|
||||
watchers []chan string
|
||||
watcherMutex sync.Mutex
|
||||
|
||||
// 加载时间
|
||||
lastLoadTime time.Time
|
||||
}
|
||||
|
||||
// ConfigItem 配置项结构
|
||||
type ConfigItem struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Group string `json:"group"` // 配置分组
|
||||
Category string `json:"category"` // 配置分类
|
||||
IsSensitive bool `json:"is_sensitive"` // 是否是敏感信息
|
||||
}
|
||||
|
||||
// ConfigGroup 配置分组
|
||||
type ConfigGroup string
|
||||
|
||||
const (
|
||||
GroupDatabase ConfigGroup = "database"
|
||||
GroupServer ConfigGroup = "server"
|
||||
GroupSecurity ConfigGroup = "security"
|
||||
GroupSearch ConfigGroup = "search"
|
||||
GroupTelegram ConfigGroup = "telegram"
|
||||
GroupCache ConfigGroup = "cache"
|
||||
GroupMeilisearch ConfigGroup = "meilisearch"
|
||||
GroupSEO ConfigGroup = "seo"
|
||||
GroupAutoProcess ConfigGroup = "auto_process"
|
||||
GroupOther ConfigGroup = "other"
|
||||
)
|
||||
|
||||
// NewConfigManager 创建配置管理器
|
||||
func NewConfigManager(repoManager *repo.RepositoryManager) *ConfigManager {
|
||||
cm := &ConfigManager{
|
||||
repo: repoManager,
|
||||
cache: make(map[string]*ConfigItem),
|
||||
configUpdateCh: make(chan string, 100), // 缓冲通道防止阻塞
|
||||
}
|
||||
|
||||
// 启动配置更新监听器
|
||||
go cm.startConfigUpdateListener()
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// startConfigUpdateListener 启动配置更新监听器
|
||||
func (cm *ConfigManager) startConfigUpdateListener() {
|
||||
for key := range cm.configUpdateCh {
|
||||
cm.notifyWatchers(key)
|
||||
}
|
||||
}
|
||||
|
||||
// notifyWatchers 通知所有监听器配置已更新
|
||||
func (cm *ConfigManager) notifyWatchers(key string) {
|
||||
cm.watcherMutex.Lock()
|
||||
defer cm.watcherMutex.Unlock()
|
||||
|
||||
for _, watcher := range cm.watchers {
|
||||
select {
|
||||
case watcher <- key:
|
||||
default:
|
||||
// 如果通道阻塞,跳过该监听器
|
||||
utils.Warn("配置监听器通道阻塞,跳过通知: %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddConfigWatcher 添加配置变更监听器
|
||||
func (cm *ConfigManager) AddConfigWatcher() chan string {
|
||||
cm.watcherMutex.Lock()
|
||||
defer cm.watcherMutex.Unlock()
|
||||
|
||||
watcher := make(chan string, 10) // 为每个监听器创建缓冲通道
|
||||
cm.watchers = append(cm.watchers, watcher)
|
||||
return watcher
|
||||
}
|
||||
|
||||
// GetConfig 获取配置项
|
||||
func (cm *ConfigManager) GetConfig(key string) (*ConfigItem, error) {
|
||||
// 先尝试从内存缓存获取
|
||||
item, exists := cm.getCachedConfig(key)
|
||||
if exists {
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// 如果缓存中没有,从数据库获取
|
||||
config, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 将数据库配置转换为ConfigItem并缓存
|
||||
item = &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||
|
||||
// 缓存配置
|
||||
cm.setCachedConfig(key, item)
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// GetConfigValue 获取配置值
|
||||
func (cm *ConfigManager) GetConfigValue(key string) (string, error) {
|
||||
item, err := cm.GetConfig(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return item.Value, nil
|
||||
}
|
||||
|
||||
// GetConfigBool 获取布尔值配置
|
||||
func (cm *ConfigManager) GetConfigBool(key string) (bool, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch strings.ToLower(value) {
|
||||
case "true", "1", "yes", "on":
|
||||
return true, nil
|
||||
case "false", "0", "no", "off", "":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("无法将配置值 '%s' 转换为布尔值", value)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigInt 获取整数值配置
|
||||
func (cm *ConfigManager) GetConfigInt(key string) (int, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.Atoi(value)
|
||||
}
|
||||
|
||||
// GetConfigInt64 获取64位整数值配置
|
||||
func (cm *ConfigManager) GetConfigInt64(key string) (int64, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
|
||||
// GetConfigFloat64 获取浮点数配置
|
||||
func (cm *ConfigManager) GetConfigFloat64(key string) (float64, error) {
|
||||
value, err := cm.GetConfigValue(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
|
||||
// SetConfig 设置配置值
|
||||
func (cm *ConfigManager) SetConfig(key, value string) error {
|
||||
// 更新数据库
|
||||
config := &entity.SystemConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Type: "string", // 默认类型,实际类型应该从现有配置中获取
|
||||
}
|
||||
|
||||
// 获取现有配置以确定类型
|
||||
existing, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||
if err == nil {
|
||||
config.Type = existing.Type
|
||||
} else {
|
||||
// 如果配置不存在,尝试从默认配置中获取类型
|
||||
config.Type = cm.getDefaultConfigType(key)
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
err = cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
item := &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||
|
||||
cm.setCachedConfig(key, item)
|
||||
|
||||
// 发送更新通知
|
||||
cm.configUpdateCh <- key
|
||||
|
||||
utils.Info("配置已更新: %s = %s", key, value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConfigWithType 设置配置值(指定类型)
|
||||
func (cm *ConfigManager) SetConfigWithType(key, value, configType string) error {
|
||||
config := &entity.SystemConfig{
|
||||
Key: key,
|
||||
Value: value,
|
||||
Type: configType,
|
||||
}
|
||||
|
||||
err := cm.repo.SystemConfigRepository.UpsertConfigs([]entity.SystemConfig{*config})
|
||||
if err != nil {
|
||||
return fmt.Errorf("保存配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
item := &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(key)
|
||||
|
||||
cm.setCachedConfig(key, item)
|
||||
|
||||
// 发送更新通知
|
||||
cm.configUpdateCh <- key
|
||||
|
||||
utils.Info("配置已更新: %s = %s (type: %s)", key, value, configType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getGroupByConfigKey 根据配置键获取分组
|
||||
func (cm *ConfigManager) getGroupByConfigKey(key string) ConfigGroup {
|
||||
switch {
|
||||
case strings.HasPrefix(key, "database_"), strings.HasPrefix(key, "db_"):
|
||||
return GroupDatabase
|
||||
case strings.HasPrefix(key, "server_"), strings.HasPrefix(key, "port"), strings.HasPrefix(key, "host"):
|
||||
return GroupServer
|
||||
case strings.HasPrefix(key, "api_"), strings.HasPrefix(key, "jwt_"), strings.HasPrefix(key, "password"):
|
||||
return GroupSecurity
|
||||
case strings.Contains(key, "meilisearch"):
|
||||
return GroupMeilisearch
|
||||
case strings.Contains(key, "telegram"):
|
||||
return GroupTelegram
|
||||
case strings.Contains(key, "cache"), strings.Contains(key, "redis"):
|
||||
return GroupCache
|
||||
case strings.Contains(key, "seo"), strings.Contains(key, "title"), strings.Contains(key, "keyword"):
|
||||
return GroupSEO
|
||||
case strings.Contains(key, "auto_"):
|
||||
return GroupAutoProcess
|
||||
case strings.Contains(key, "forbidden"), strings.Contains(key, "ad_"):
|
||||
return GroupOther
|
||||
default:
|
||||
return GroupOther
|
||||
}
|
||||
}
|
||||
|
||||
// getCategoryByConfigKey 根据配置键获取分类
|
||||
func (cm *ConfigManager) getCategoryByConfigKey(key string) string {
|
||||
switch {
|
||||
case key == entity.ConfigKeySiteTitle || key == entity.ConfigKeySiteDescription:
|
||||
return "basic_info"
|
||||
case key == entity.ConfigKeyKeywords || key == entity.ConfigKeyAuthor:
|
||||
return "seo"
|
||||
case key == entity.ConfigKeyAutoProcessReadyResources || key == entity.ConfigKeyAutoProcessInterval:
|
||||
return "auto_process"
|
||||
case key == entity.ConfigKeyAutoTransferEnabled || key == entity.ConfigKeyAutoTransferLimitDays:
|
||||
return "auto_transfer"
|
||||
case key == entity.ConfigKeyMeilisearchEnabled || key == entity.ConfigKeyMeilisearchHost:
|
||||
return "search"
|
||||
case key == entity.ConfigKeyTelegramBotEnabled || key == entity.ConfigKeyTelegramBotApiKey:
|
||||
return "telegram"
|
||||
case key == entity.ConfigKeyMaintenanceMode || key == entity.ConfigKeyEnableRegister:
|
||||
return "system"
|
||||
case key == entity.ConfigKeyForbiddenWords || key == entity.ConfigKeyAdKeywords:
|
||||
return "filtering"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
// isSensitiveConfig 判断是否是敏感配置
|
||||
func (cm *ConfigManager) isSensitiveConfig(key string) bool {
|
||||
switch key {
|
||||
case entity.ConfigKeyApiToken,
|
||||
entity.ConfigKeyMeilisearchMasterKey,
|
||||
entity.ConfigKeyTelegramBotApiKey,
|
||||
entity.ConfigKeyTelegramProxyUsername,
|
||||
entity.ConfigKeyTelegramProxyPassword:
|
||||
return true
|
||||
default:
|
||||
return strings.Contains(strings.ToLower(key), "password") ||
|
||||
strings.Contains(strings.ToLower(key), "secret") ||
|
||||
strings.Contains(strings.ToLower(key), "key") ||
|
||||
strings.Contains(strings.ToLower(key), "token")
|
||||
}
|
||||
}
|
||||
|
||||
// getDefaultConfigType 获取默认配置类型
|
||||
func (cm *ConfigManager) getDefaultConfigType(key string) string {
|
||||
switch key {
|
||||
case entity.ConfigKeyAutoProcessReadyResources,
|
||||
entity.ConfigKeyAutoTransferEnabled,
|
||||
entity.ConfigKeyAutoFetchHotDramaEnabled,
|
||||
entity.ConfigKeyMaintenanceMode,
|
||||
entity.ConfigKeyEnableRegister,
|
||||
entity.ConfigKeyMeilisearchEnabled,
|
||||
entity.ConfigKeyTelegramBotEnabled:
|
||||
return entity.ConfigTypeBool
|
||||
case entity.ConfigKeyAutoProcessInterval,
|
||||
entity.ConfigKeyAutoTransferLimitDays,
|
||||
entity.ConfigKeyAutoTransferMinSpace,
|
||||
entity.ConfigKeyPageSize:
|
||||
return entity.ConfigTypeInt
|
||||
case entity.ConfigKeyAnnouncements:
|
||||
return entity.ConfigTypeJSON
|
||||
default:
|
||||
return entity.ConfigTypeString
|
||||
}
|
||||
}
|
||||
|
||||
// LoadAllConfigs 加载所有配置到缓存
|
||||
func (cm *ConfigManager) LoadAllConfigs() error {
|
||||
configs, err := cm.repo.SystemConfigRepository.FindAll()
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载所有配置失败: %v", err)
|
||||
}
|
||||
|
||||
cm.cacheMutex.Lock()
|
||||
defer cm.cacheMutex.Unlock()
|
||||
|
||||
// 清空现有缓存
|
||||
cm.cache = make(map[string]*ConfigItem)
|
||||
|
||||
// 更新缓存
|
||||
for _, config := range configs {
|
||||
item := &ConfigItem{
|
||||
Key: config.Key,
|
||||
Value: config.Value,
|
||||
Type: config.Type,
|
||||
UpdatedAt: time.Now(), // 实际应该从数据库获取
|
||||
}
|
||||
|
||||
if group := cm.getGroupByConfigKey(config.Key); group != "" {
|
||||
item.Group = string(group)
|
||||
}
|
||||
|
||||
if category := cm.getCategoryByConfigKey(config.Key); category != "" {
|
||||
item.Category = category
|
||||
}
|
||||
|
||||
item.IsSensitive = cm.isSensitiveConfig(config.Key)
|
||||
|
||||
cm.cache[config.Key] = item
|
||||
}
|
||||
|
||||
cm.lastLoadTime = time.Now()
|
||||
|
||||
utils.Info("已加载 %d 个配置项到缓存", len(configs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshConfigCache 刷新配置缓存
|
||||
func (cm *ConfigManager) RefreshConfigCache() error {
|
||||
return cm.LoadAllConfigs()
|
||||
}
|
||||
|
||||
// GetCachedConfig 获取缓存的配置
|
||||
func (cm *ConfigManager) getCachedConfig(key string) (*ConfigItem, bool) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
item, exists := cm.cache[key]
|
||||
return item, exists
|
||||
}
|
||||
|
||||
// setCachedConfig 设置缓存的配置
|
||||
func (cm *ConfigManager) setCachedConfig(key string, item *ConfigItem) {
|
||||
cm.cacheMutex.Lock()
|
||||
defer cm.cacheMutex.Unlock()
|
||||
|
||||
cm.cache[key] = item
|
||||
}
|
||||
|
||||
// GetConfigByGroup 按分组获取配置
|
||||
func (cm *ConfigManager) GetConfigByGroup(group ConfigGroup) (map[string]*ConfigItem, error) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
result := make(map[string]*ConfigItem)
|
||||
|
||||
for key, item := range cm.cache {
|
||||
if ConfigGroup(item.Group) == group {
|
||||
result[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetConfigByCategory 按分类获取配置
|
||||
func (cm *ConfigManager) GetConfigByCategory(category string) (map[string]*ConfigItem, error) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
result := make(map[string]*ConfigItem)
|
||||
|
||||
for key, item := range cm.cache {
|
||||
if item.Category == category {
|
||||
result[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DeleteConfig 删除配置
|
||||
func (cm *ConfigManager) DeleteConfig(key string) error {
|
||||
// 先查找配置获取ID
|
||||
config, err := cm.repo.SystemConfigRepository.FindByKey(key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 从数据库删除
|
||||
err = cm.repo.SystemConfigRepository.Delete(config.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 从缓存中移除
|
||||
cm.cacheMutex.Lock()
|
||||
delete(cm.cache, key)
|
||||
cm.cacheMutex.Unlock()
|
||||
|
||||
utils.Info("配置已删除: %s", key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSensitiveConfigKeys 获取所有敏感配置键
|
||||
func (cm *ConfigManager) GetSensitiveConfigKeys() []string {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
var sensitiveKeys []string
|
||||
for key, item := range cm.cache {
|
||||
if item.IsSensitive {
|
||||
sensitiveKeys = append(sensitiveKeys, key)
|
||||
}
|
||||
}
|
||||
|
||||
return sensitiveKeys
|
||||
}
|
||||
|
||||
// GetConfigWithMask 获取配置值(敏感配置会被遮蔽)
|
||||
func (cm *ConfigManager) GetConfigWithMask(key string) (*ConfigItem, error) {
|
||||
item, err := cm.GetConfig(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if item.IsSensitive {
|
||||
// 创建副本并遮蔽敏感值
|
||||
maskedItem := *item
|
||||
maskedItem.Value = cm.maskSensitiveValue(item.Value)
|
||||
return &maskedItem, nil
|
||||
}
|
||||
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// maskSensitiveValue 遮蔽敏感值
|
||||
func (cm *ConfigManager) maskSensitiveValue(value string) string {
|
||||
if len(value) <= 4 {
|
||||
return "****"
|
||||
}
|
||||
|
||||
// 保留前2个和后2个字符,中间用****替代
|
||||
return value[:2] + "****" + value[len(value)-2:]
|
||||
}
|
||||
|
||||
// GetConfigAsJSON 获取配置为JSON格式
|
||||
func (cm *ConfigManager) GetConfigAsJSON() ([]byte, error) {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
// 创建副本,敏感配置使用遮蔽值
|
||||
configMap := make(map[string]*ConfigItem)
|
||||
for key, item := range cm.cache {
|
||||
if item.IsSensitive {
|
||||
maskedItem := *item
|
||||
maskedItem.Value = cm.maskSensitiveValue(item.Value)
|
||||
configMap[key] = &maskedItem
|
||||
} else {
|
||||
configMap[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
return json.MarshalIndent(configMap, "", " ")
|
||||
}
|
||||
|
||||
// GetConfigStatistics 获取配置统计信息
|
||||
func (cm *ConfigManager) GetConfigStatistics() map[string]interface{} {
|
||||
cm.cacheMutex.RLock()
|
||||
defer cm.cacheMutex.RUnlock()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total_configs": len(cm.cache),
|
||||
"last_load_time": cm.lastLoadTime,
|
||||
"cache_size_bytes": len(cm.cache) * 100, // 估算每个配置约100字节
|
||||
"groups": make(map[string]int),
|
||||
"types": make(map[string]int),
|
||||
"categories": make(map[string]int),
|
||||
"sensitive_configs": 0,
|
||||
"config_keys": make([]string, 0),
|
||||
}
|
||||
|
||||
groups := make(map[string]int)
|
||||
types := make(map[string]int)
|
||||
categories := make(map[string]int)
|
||||
|
||||
for key, item := range cm.cache {
|
||||
// 统计分组
|
||||
groups[item.Group]++
|
||||
|
||||
// 统计类型
|
||||
types[item.Type]++
|
||||
|
||||
// 统计分类
|
||||
categories[item.Category]++
|
||||
|
||||
// 统计敏感配置
|
||||
if item.IsSensitive {
|
||||
stats["sensitive_configs"] = stats["sensitive_configs"].(int) + 1
|
||||
}
|
||||
|
||||
// 添加配置键到列表
|
||||
keys := stats["config_keys"].([]string)
|
||||
keys = append(keys, key)
|
||||
stats["config_keys"] = keys
|
||||
}
|
||||
|
||||
stats["groups"] = groups
|
||||
stats["types"] = types
|
||||
stats["categories"] = categories
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// GetEnvironmentConfig 从环境变量获取配置
|
||||
func (cm *ConfigManager) GetEnvironmentConfig(key string) (string, bool) {
|
||||
value := os.Getenv(key)
|
||||
if value != "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
// 尝试使用大写版本的键
|
||||
value = os.Getenv(strings.ToUpper(key))
|
||||
if value != "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
// 尝试使用大写带下划线的格式
|
||||
upperKey := strings.ToUpper(strings.ReplaceAll(key, ".", "_"))
|
||||
value = os.Getenv(upperKey)
|
||||
if value != "" {
|
||||
return value, true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetConfigWithEnvFallback 获取配置,环境变量优先
|
||||
func (cm *ConfigManager) GetConfigWithEnvFallback(configKey, envKey string) (string, error) {
|
||||
// 优先从环境变量获取
|
||||
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||
return envValue, nil
|
||||
}
|
||||
|
||||
// 如果环境变量不存在,从数据库获取
|
||||
return cm.GetConfigValue(configKey)
|
||||
}
|
||||
|
||||
// GetConfigIntWithEnvFallback 获取整数配置,环境变量优先
|
||||
func (cm *ConfigManager) GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) {
|
||||
// 优先从环境变量获取
|
||||
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||
return strconv.Atoi(envValue)
|
||||
}
|
||||
|
||||
// 如果环境变量不存在,从数据库获取
|
||||
return cm.GetConfigInt(configKey)
|
||||
}
|
||||
|
||||
// GetConfigBoolWithEnvFallback 获取布尔配置,环境变量优先
|
||||
func (cm *ConfigManager) GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) {
|
||||
// 优先从环境变量获取
|
||||
if envValue, exists := cm.GetEnvironmentConfig(envKey); exists {
|
||||
switch strings.ToLower(envValue) {
|
||||
case "true", "1", "yes", "on":
|
||||
return true, nil
|
||||
case "false", "0", "no", "off", "":
|
||||
return false, nil
|
||||
default:
|
||||
return false, fmt.Errorf("无法将环境变量值 '%s' 转换为布尔值", envValue)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果环境变量不存在,从数据库获取
|
||||
return cm.GetConfigBool(configKey)
|
||||
}
|
||||
124
config/global.go
Normal file
124
config/global.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
globalConfigManager *ConfigManager
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// SetGlobalConfigManager 设置全局配置管理器
|
||||
func SetGlobalConfigManager(cm *ConfigManager) {
|
||||
globalConfigManager = cm
|
||||
}
|
||||
|
||||
// GetGlobalConfigManager 获取全局配置管理器
|
||||
func GetGlobalConfigManager() *ConfigManager {
|
||||
return globalConfigManager
|
||||
}
|
||||
|
||||
// GetConfig 获取配置值(全局函数)
|
||||
func GetConfig(key string) (*ConfigItem, error) {
|
||||
if globalConfigManager == nil {
|
||||
return nil, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfig(key)
|
||||
}
|
||||
|
||||
// GetConfigValue 获取配置值(全局函数)
|
||||
func GetConfigValue(key string) (string, error) {
|
||||
if globalConfigManager == nil {
|
||||
return "", ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigValue(key)
|
||||
}
|
||||
|
||||
// GetConfigBool 获取布尔配置值(全局函数)
|
||||
func GetConfigBool(key string) (bool, error) {
|
||||
if globalConfigManager == nil {
|
||||
return false, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigBool(key)
|
||||
}
|
||||
|
||||
// GetConfigInt 获取整数配置值(全局函数)
|
||||
func GetConfigInt(key string) (int, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigInt(key)
|
||||
}
|
||||
|
||||
// GetConfigInt64 获取64位整数配置值(全局函数)
|
||||
func GetConfigInt64(key string) (int64, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigInt64(key)
|
||||
}
|
||||
|
||||
// GetConfigFloat64 获取浮点数配置值(全局函数)
|
||||
func GetConfigFloat64(key string) (float64, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigFloat64(key)
|
||||
}
|
||||
|
||||
// SetConfig 设置配置值(全局函数)
|
||||
func SetConfig(key, value string) error {
|
||||
if globalConfigManager == nil {
|
||||
return ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.SetConfig(key, value)
|
||||
}
|
||||
|
||||
// SetConfigWithType 设置配置值(指定类型,全局函数)
|
||||
func SetConfigWithType(key, value, configType string) error {
|
||||
if globalConfigManager == nil {
|
||||
return ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.SetConfigWithType(key, value, configType)
|
||||
}
|
||||
|
||||
// GetConfigWithEnvFallback 获取配置值(环境变量优先,全局函数)
|
||||
func GetConfigWithEnvFallback(configKey, envKey string) (string, error) {
|
||||
if globalConfigManager == nil {
|
||||
return "", ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigWithEnvFallback(configKey, envKey)
|
||||
}
|
||||
|
||||
// GetConfigIntWithEnvFallback 获取整数配置值(环境变量优先,全局函数)
|
||||
func GetConfigIntWithEnvFallback(configKey, envKey string) (int, error) {
|
||||
if globalConfigManager == nil {
|
||||
return 0, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigIntWithEnvFallback(configKey, envKey)
|
||||
}
|
||||
|
||||
// GetConfigBoolWithEnvFallback 获取布尔配置值(环境变量优先,全局函数)
|
||||
func GetConfigBoolWithEnvFallback(configKey, envKey string) (bool, error) {
|
||||
if globalConfigManager == nil {
|
||||
return false, ErrConfigManagerNotInitialized
|
||||
}
|
||||
return globalConfigManager.GetConfigBoolWithEnvFallback(configKey, envKey)
|
||||
}
|
||||
|
||||
// ErrConfigManagerNotInitialized 配置管理器未初始化错误
|
||||
var ErrConfigManagerNotInitialized = &ConfigError{
|
||||
Code: "CONFIG_MANAGER_NOT_INITIALIZED",
|
||||
Message: "配置管理器未初始化",
|
||||
}
|
||||
|
||||
// ConfigError 配置错误
|
||||
type ConfigError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ConfigError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
31
config/sync.go
Normal file
31
config/sync.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// SyncWithRepository 同步配置管理器与Repository的缓存
|
||||
func (cm *ConfigManager) SyncWithRepository(repoManager *repo.RepositoryManager) {
|
||||
// 监听配置变更事件并同步缓存
|
||||
// 这是一个抽象概念,实际实现需要修改Repository接口
|
||||
|
||||
// 当配置更新时,通知Repository清理缓存
|
||||
go func() {
|
||||
watcher := cm.AddConfigWatcher()
|
||||
for {
|
||||
select {
|
||||
case key := <-watcher:
|
||||
// 通知Repository层清理缓存(如果Repository支持)
|
||||
utils.Debug("配置 %s 已更新,可能需要同步到Repository缓存", key)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// UpdateRepositoryCache 当配置管理器更新配置时,通知Repository层同步
|
||||
func (cm *ConfigManager) UpdateRepositoryCache(repoManager *repo.RepositoryManager) {
|
||||
// 这个函数需要Repository支持特定的缓存清理方法
|
||||
// 由于现有Repository没有提供这样的接口,我们只能依赖数据库同步
|
||||
utils.Info("配置已通过配置管理器更新,Repository层将从数据库重新加载")
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
@@ -45,8 +47,22 @@ func InitDB() error {
|
||||
host, port, user, password, dbname)
|
||||
|
||||
var err error
|
||||
// 配置慢查询日志
|
||||
slowThreshold := getEnvInt("DB_SLOW_THRESHOLD_MS", 200)
|
||||
logLevel := logger.Info
|
||||
if os.Getenv("ENV") == "production" {
|
||||
logLevel = logger.Warn
|
||||
}
|
||||
|
||||
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Info),
|
||||
Logger: logger.New(
|
||||
log.New(os.Stdout, "\r\n", log.LstdFlags),
|
||||
logger.Config{
|
||||
SlowThreshold: time.Duration(slowThreshold) * time.Millisecond,
|
||||
LogLevel: logLevel,
|
||||
Colorful: true,
|
||||
},
|
||||
),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -58,10 +74,17 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置连接池参数
|
||||
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
|
||||
sqlDB.SetMaxOpenConns(100) // 最大打开连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
|
||||
// 优化数据库连接池参数
|
||||
maxOpenConns := getEnvInt("DB_MAX_OPEN_CONNS", 50)
|
||||
maxIdleConns := getEnvInt("DB_MAX_IDLE_CONNS", 20)
|
||||
connMaxLifetime := getEnvInt("DB_CONN_MAX_LIFETIME_MINUTES", 30)
|
||||
|
||||
sqlDB.SetMaxOpenConns(maxOpenConns) // 最大打开连接数
|
||||
sqlDB.SetMaxIdleConns(maxIdleConns) // 最大空闲连接数
|
||||
sqlDB.SetConnMaxLifetime(time.Duration(connMaxLifetime) * time.Minute) // 连接最大生命周期
|
||||
|
||||
utils.Info("数据库连接池配置 - 最大连接: %d, 空闲连接: %d, 生命周期: %d分钟",
|
||||
maxOpenConns, maxIdleConns, connMaxLifetime)
|
||||
|
||||
// 检查是否需要迁移(只在开发环境或首次启动时)
|
||||
if shouldRunMigration() {
|
||||
@@ -83,6 +106,9 @@ func InitDB() error {
|
||||
&entity.TaskItem{},
|
||||
&entity.File{},
|
||||
&entity.TelegramChannel{},
|
||||
&entity.APIAccessLog{},
|
||||
&entity.APIAccessLogStats{},
|
||||
&entity.APIAccessLogSummary{},
|
||||
)
|
||||
if err != nil {
|
||||
utils.Fatal("数据库迁移失败: %v", err)
|
||||
@@ -297,3 +323,19 @@ func insertDefaultDataIfEmpty() error {
|
||||
utils.Info("默认数据插入完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getEnvInt 获取环境变量中的整数值,如果不存在则返回默认值
|
||||
func getEnvInt(key string, defaultValue int) int {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
intValue, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
utils.Warn("环境变量 %s 的值 '%s' 不是有效的整数,使用默认值 %d", key, value, defaultValue)
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return intValue
|
||||
}
|
||||
|
||||
66
db/converter/api_access_log_converter.go
Normal file
66
db/converter/api_access_log_converter.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// ToAPIAccessLogResponse 将APIAccessLog实体转换为APIAccessLogResponse
|
||||
func ToAPIAccessLogResponse(log *entity.APIAccessLog) dto.APIAccessLogResponse {
|
||||
return dto.APIAccessLogResponse{
|
||||
ID: log.ID,
|
||||
IP: log.IP,
|
||||
UserAgent: log.UserAgent,
|
||||
Endpoint: log.Endpoint,
|
||||
Method: log.Method,
|
||||
RequestParams: log.RequestParams,
|
||||
ResponseStatus: log.ResponseStatus,
|
||||
ResponseData: log.ResponseData,
|
||||
ProcessCount: log.ProcessCount,
|
||||
ErrorMessage: log.ErrorMessage,
|
||||
ProcessingTime: log.ProcessingTime,
|
||||
CreatedAt: log.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIAccessLogResponseList 将APIAccessLog实体列表转换为APIAccessLogResponse列表
|
||||
func ToAPIAccessLogResponseList(logs []entity.APIAccessLog) []dto.APIAccessLogResponse {
|
||||
responses := make([]dto.APIAccessLogResponse, len(logs))
|
||||
for i, log := range logs {
|
||||
responses[i] = ToAPIAccessLogResponse(&log)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
// ToAPIAccessLogSummaryResponse 将APIAccessLogSummary实体转换为APIAccessLogSummaryResponse
|
||||
func ToAPIAccessLogSummaryResponse(summary *entity.APIAccessLogSummary) dto.APIAccessLogSummaryResponse {
|
||||
return dto.APIAccessLogSummaryResponse{
|
||||
TotalRequests: summary.TotalRequests,
|
||||
TodayRequests: summary.TodayRequests,
|
||||
WeekRequests: summary.WeekRequests,
|
||||
MonthRequests: summary.MonthRequests,
|
||||
ErrorRequests: summary.ErrorRequests,
|
||||
UniqueIPs: summary.UniqueIPs,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIAccessLogStatsResponse 将APIAccessLogStats实体转换为APIAccessLogStatsResponse
|
||||
func ToAPIAccessLogStatsResponse(stat entity.APIAccessLogStats) dto.APIAccessLogStatsResponse {
|
||||
return dto.APIAccessLogStatsResponse{
|
||||
Endpoint: stat.Endpoint,
|
||||
Method: stat.Method,
|
||||
RequestCount: stat.RequestCount,
|
||||
ErrorCount: stat.ErrorCount,
|
||||
AvgProcessTime: stat.AvgProcessTime,
|
||||
LastAccess: stat.LastAccess,
|
||||
}
|
||||
}
|
||||
|
||||
// ToAPIAccessLogStatsResponseList 将APIAccessLogStats实体列表转换为APIAccessLogStatsResponse列表
|
||||
func ToAPIAccessLogStatsResponseList(stats []entity.APIAccessLogStats) []dto.APIAccessLogStatsResponse {
|
||||
responses := make([]dto.APIAccessLogStatsResponse, len(stats))
|
||||
for i, stat := range stats {
|
||||
responses[i] = ToAPIAccessLogStatsResponse(stat)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
@@ -73,6 +73,9 @@ func ToResourceResponseFromMeilisearch(doc interface{}) dto.ResourceResponse {
|
||||
if urlField := docValue.FieldByName("URL"); urlField.IsValid() {
|
||||
response.URL = urlField.String()
|
||||
}
|
||||
if coverField := docValue.FieldByName("Cover"); coverField.IsValid() {
|
||||
response.Cover = coverField.String()
|
||||
}
|
||||
if saveURLField := docValue.FieldByName("SaveURL"); saveURLField.IsValid() {
|
||||
response.SaveURL = saveURLField.String()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -90,6 +91,27 @@ func SystemConfigToResponse(configs []entity.SystemConfig) *dto.SystemConfigResp
|
||||
response.MeilisearchMasterKey = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
response.MeilisearchIndexName = config.Value
|
||||
case entity.ConfigKeyEnableAnnouncements:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.EnableAnnouncements = val
|
||||
}
|
||||
case entity.ConfigKeyAnnouncements:
|
||||
if config.Value == "" || config.Value == "[]" {
|
||||
response.Announcements = ""
|
||||
} else {
|
||||
// 在响应时保持为字符串,后续由前端处理
|
||||
response.Announcements = config.Value
|
||||
}
|
||||
case entity.ConfigKeyEnableFloatButtons:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.EnableFloatButtons = val
|
||||
}
|
||||
case entity.ConfigKeyWechatSearchImage:
|
||||
response.WechatSearchImage = config.Value
|
||||
case entity.ConfigKeyTelegramQrImage:
|
||||
response.TelegramQrImage = config.Value
|
||||
case entity.ConfigKeyQrCodeStyle:
|
||||
response.QrCodeStyle = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,6 +243,35 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig {
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchIndexName)
|
||||
}
|
||||
|
||||
// 界面配置处理
|
||||
if req.EnableAnnouncements != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableAnnouncements, Value: strconv.FormatBool(*req.EnableAnnouncements), Type: entity.ConfigTypeBool})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyEnableAnnouncements)
|
||||
}
|
||||
if req.Announcements != nil {
|
||||
// 将数组转换为JSON字符串
|
||||
if jsonBytes, err := json.Marshal(*req.Announcements); err == nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyAnnouncements, Value: string(jsonBytes), Type: entity.ConfigTypeJSON})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyAnnouncements)
|
||||
}
|
||||
}
|
||||
if req.EnableFloatButtons != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyEnableFloatButtons, Value: strconv.FormatBool(*req.EnableFloatButtons), Type: entity.ConfigTypeBool})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyEnableFloatButtons)
|
||||
}
|
||||
if req.WechatSearchImage != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyWechatSearchImage, Value: *req.WechatSearchImage, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyWechatSearchImage)
|
||||
}
|
||||
if req.TelegramQrImage != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyTelegramQrImage, Value: *req.TelegramQrImage, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyTelegramQrImage)
|
||||
}
|
||||
if req.QrCodeStyle != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyQrCodeStyle, Value: *req.QrCodeStyle, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyQrCodeStyle)
|
||||
}
|
||||
|
||||
// 记录更新的配置项
|
||||
if len(updatedKeys) > 0 {
|
||||
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
||||
@@ -332,6 +383,26 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
response[entity.ConfigResponseFieldMeilisearchMasterKey] = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
response[entity.ConfigResponseFieldMeilisearchIndexName] = config.Value
|
||||
case entity.ConfigKeyEnableAnnouncements:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response["enable_announcements"] = val
|
||||
}
|
||||
case entity.ConfigKeyAnnouncements:
|
||||
if config.Value == "" || config.Value == "[]" {
|
||||
response["announcements"] = ""
|
||||
} else {
|
||||
response["announcements"] = config.Value
|
||||
}
|
||||
case entity.ConfigKeyEnableFloatButtons:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response["enable_float_buttons"] = val
|
||||
}
|
||||
case entity.ConfigKeyWechatSearchImage:
|
||||
response["wechat_search_image"] = config.Value
|
||||
case entity.ConfigKeyTelegramQrImage:
|
||||
response["telegram_qr_image"] = config.Value
|
||||
case entity.ConfigKeyQrCodeStyle:
|
||||
response["qr_code_style"] = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,5 +443,11 @@ func getDefaultConfigResponse() *dto.SystemConfigResponse {
|
||||
MeilisearchPort: entity.ConfigDefaultMeilisearchPort,
|
||||
MeilisearchMasterKey: entity.ConfigDefaultMeilisearchMasterKey,
|
||||
MeilisearchIndexName: entity.ConfigDefaultMeilisearchIndexName,
|
||||
EnableAnnouncements: false,
|
||||
Announcements: "",
|
||||
EnableFloatButtons: false,
|
||||
WechatSearchImage: entity.ConfigDefaultWechatSearchImage,
|
||||
TelegramQrImage: entity.ConfigDefaultTelegramQrImage,
|
||||
QrCodeStyle: entity.ConfigDefaultQrCodeStyle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ func TelegramChannelToResponse(channel entity.TelegramChannel) dto.TelegramChann
|
||||
ContentCategories: channel.ContentCategories,
|
||||
ContentTags: channel.ContentTags,
|
||||
IsActive: channel.IsActive,
|
||||
ResourceStrategy: channel.ResourceStrategy,
|
||||
TimeLimit: channel.TimeLimit,
|
||||
LastPushAt: channel.LastPushAt,
|
||||
RegisteredBy: channel.RegisteredBy,
|
||||
RegisteredAt: channel.RegisteredAt,
|
||||
@@ -41,7 +43,7 @@ func TelegramChannelsToResponse(channels []entity.TelegramChannel) []dto.Telegra
|
||||
|
||||
// RequestToTelegramChannel 将请求DTO转换为TelegramChannel实体
|
||||
func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy string) entity.TelegramChannel {
|
||||
return entity.TelegramChannel{
|
||||
channel := entity.TelegramChannel{
|
||||
ChatID: req.ChatID,
|
||||
ChatName: req.ChatName,
|
||||
ChatType: req.ChatType,
|
||||
@@ -55,6 +57,21 @@ func RequestToTelegramChannel(req dto.TelegramChannelRequest, registeredBy strin
|
||||
RegisteredBy: registeredBy,
|
||||
RegisteredAt: time.Now(),
|
||||
}
|
||||
|
||||
// 设置默认值(如果为空)
|
||||
if req.ResourceStrategy == "" {
|
||||
channel.ResourceStrategy = "random"
|
||||
} else {
|
||||
channel.ResourceStrategy = req.ResourceStrategy
|
||||
}
|
||||
|
||||
if req.TimeLimit == "" {
|
||||
channel.TimeLimit = "none"
|
||||
} else {
|
||||
channel.TimeLimit = req.TimeLimit
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
// TelegramBotConfigToResponse 将Telegram bot配置转换为响应DTO
|
||||
|
||||
88
db/converter/wechat_bot_converter.go
Normal file
88
db/converter/wechat_bot_converter.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
|
||||
// WechatBotConfigRequestToSystemConfigs 将微信机器人配置请求转换为系统配置实体
|
||||
func WechatBotConfigRequestToSystemConfigs(req dto.WechatBotConfigRequest) []entity.SystemConfig {
|
||||
configs := []entity.SystemConfig{
|
||||
{Key: entity.ConfigKeyWechatBotEnabled, Value: wechatBoolToString(req.Enabled)},
|
||||
{Key: entity.ConfigKeyWechatAppId, Value: req.AppID},
|
||||
{Key: entity.ConfigKeyWechatAppSecret, Value: req.AppSecret},
|
||||
{Key: entity.ConfigKeyWechatToken, Value: req.Token},
|
||||
{Key: entity.ConfigKeyWechatEncodingAesKey, Value: req.EncodingAesKey},
|
||||
{Key: entity.ConfigKeyWechatWelcomeMessage, Value: req.WelcomeMessage},
|
||||
{Key: entity.ConfigKeyWechatAutoReplyEnabled, Value: wechatBoolToString(req.AutoReplyEnabled)},
|
||||
{Key: entity.ConfigKeyWechatSearchLimit, Value: wechatIntToString(req.SearchLimit)},
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
// SystemConfigToWechatBotConfig 将系统配置转换为微信机器人配置响应
|
||||
func SystemConfigToWechatBotConfig(configs []entity.SystemConfig) dto.WechatBotConfigResponse {
|
||||
resp := dto.WechatBotConfigResponse{
|
||||
Enabled: false,
|
||||
AppID: "",
|
||||
AppSecret: "",
|
||||
Token: "",
|
||||
EncodingAesKey: "",
|
||||
WelcomeMessage: "欢迎关注老九网盘资源库!发送关键词即可搜索资源。",
|
||||
AutoReplyEnabled: true,
|
||||
SearchLimit: 5,
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeyWechatBotEnabled:
|
||||
resp.Enabled = config.Value == "true"
|
||||
case entity.ConfigKeyWechatAppId:
|
||||
resp.AppID = config.Value
|
||||
case entity.ConfigKeyWechatAppSecret:
|
||||
resp.AppSecret = config.Value
|
||||
case entity.ConfigKeyWechatToken:
|
||||
resp.Token = config.Value
|
||||
case entity.ConfigKeyWechatEncodingAesKey:
|
||||
resp.EncodingAesKey = config.Value
|
||||
case entity.ConfigKeyWechatWelcomeMessage:
|
||||
if config.Value != "" {
|
||||
resp.WelcomeMessage = config.Value
|
||||
}
|
||||
case entity.ConfigKeyWechatAutoReplyEnabled:
|
||||
resp.AutoReplyEnabled = config.Value == "true"
|
||||
case entity.ConfigKeyWechatSearchLimit:
|
||||
if config.Value != "" {
|
||||
resp.SearchLimit = wechatStringToInt(config.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// 辅助函数 - 使用大写名称避免与其他文件中的函数冲突
|
||||
func wechatBoolToString(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func wechatIntToString(i int) string {
|
||||
return strconv.Itoa(i)
|
||||
}
|
||||
|
||||
func wechatStringToInt(s string) int {
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
||||
55
db/dto/api_access_log.go
Normal file
55
db/dto/api_access_log.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
|
||||
// APIAccessLogResponse API访问日志响应
|
||||
type APIAccessLogResponse struct {
|
||||
ID uint `json:"id"`
|
||||
IP string `json:"ip"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
RequestParams string `json:"request_params"`
|
||||
ResponseStatus int `json:"response_status"`
|
||||
ResponseData string `json:"response_data"`
|
||||
ProcessCount int `json:"process_count"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
ProcessingTime int64 `json:"processing_time"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// APIAccessLogSummaryResponse API访问日志汇总响应
|
||||
type APIAccessLogSummaryResponse struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
TodayRequests int64 `json:"today_requests"`
|
||||
WeekRequests int64 `json:"week_requests"`
|
||||
MonthRequests int64 `json:"month_requests"`
|
||||
ErrorRequests int64 `json:"error_requests"`
|
||||
UniqueIPs int64 `json:"unique_ips"`
|
||||
}
|
||||
|
||||
// APIAccessLogStatsResponse 按端点统计响应
|
||||
type APIAccessLogStatsResponse struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
AvgProcessTime int64 `json:"avg_process_time"`
|
||||
LastAccess time.Time `json:"last_access"`
|
||||
}
|
||||
|
||||
// APIAccessLogListResponse API访问日志列表响应
|
||||
type APIAccessLogListResponse struct {
|
||||
Data []APIAccessLogResponse `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
// APIAccessLogFilterRequest API访问日志过滤请求
|
||||
type APIAccessLogFilterRequest struct {
|
||||
StartDate string `json:"start_date,omitempty"`
|
||||
EndDate string `json:"end_date,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
IP string `json:"ip,omitempty"`
|
||||
Page int `json:"page,omitempty" default:"1"`
|
||||
PageSize int `json:"page_size,omitempty" default:"20"`
|
||||
}
|
||||
@@ -42,6 +42,14 @@ type SystemConfigRequest struct {
|
||||
MeilisearchPort *string `json:"meilisearch_port,omitempty"`
|
||||
MeilisearchMasterKey *string `json:"meilisearch_master_key,omitempty"`
|
||||
MeilisearchIndexName *string `json:"meilisearch_index_name,omitempty"`
|
||||
|
||||
// 界面配置
|
||||
EnableAnnouncements *bool `json:"enable_announcements,omitempty"`
|
||||
Announcements *[]map[string]interface{} `json:"announcements,omitempty"`
|
||||
EnableFloatButtons *bool `json:"enable_float_buttons,omitempty"`
|
||||
WechatSearchImage *string `json:"wechat_search_image,omitempty"`
|
||||
TelegramQrImage *string `json:"telegram_qr_image,omitempty"`
|
||||
QrCodeStyle *string `json:"qr_code_style,omitempty"`
|
||||
}
|
||||
|
||||
// SystemConfigResponse 系统配置响应
|
||||
@@ -90,6 +98,14 @@ type SystemConfigResponse struct {
|
||||
MeilisearchPort string `json:"meilisearch_port"`
|
||||
MeilisearchMasterKey string `json:"meilisearch_master_key"`
|
||||
MeilisearchIndexName string `json:"meilisearch_index_name"`
|
||||
|
||||
// 界面配置
|
||||
EnableAnnouncements bool `json:"enable_announcements"`
|
||||
Announcements string `json:"announcements"`
|
||||
EnableFloatButtons bool `json:"enable_float_buttons"`
|
||||
WechatSearchImage string `json:"wechat_search_image"`
|
||||
TelegramQrImage string `json:"telegram_qr_image"`
|
||||
QrCodeStyle string `json:"qr_code_style"`
|
||||
}
|
||||
|
||||
// SystemConfigItem 单个配置项
|
||||
|
||||
@@ -14,6 +14,8 @@ type TelegramChannelRequest struct {
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ResourceStrategy string `json:"resource_strategy"`
|
||||
TimeLimit string `json:"time_limit"`
|
||||
}
|
||||
|
||||
// TelegramChannelUpdateRequest 更新 Telegram 频道/群组请求(ChatID可选)
|
||||
@@ -28,6 +30,8 @@ type TelegramChannelUpdateRequest struct {
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ResourceStrategy string `json:"resource_strategy"`
|
||||
TimeLimit string `json:"time_limit"`
|
||||
}
|
||||
|
||||
// TelegramChannelResponse Telegram 频道/群组响应
|
||||
@@ -43,6 +47,8 @@ type TelegramChannelResponse struct {
|
||||
ContentCategories string `json:"content_categories"`
|
||||
ContentTags string `json:"content_tags"`
|
||||
IsActive bool `json:"is_active"`
|
||||
ResourceStrategy string `json:"resource_strategy"`
|
||||
TimeLimit string `json:"time_limit"`
|
||||
LastPushAt *time.Time `json:"last_push_at"`
|
||||
RegisteredBy string `json:"registered_by"`
|
||||
RegisteredAt time.Time `json:"registered_at"`
|
||||
|
||||
25
db/dto/wechat_bot.go
Normal file
25
db/dto/wechat_bot.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package dto
|
||||
|
||||
// WechatBotConfigRequest 微信公众号机器人配置请求
|
||||
type WechatBotConfigRequest struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"app_id"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
Token string `json:"token"`
|
||||
EncodingAesKey string `json:"encoding_aes_key"`
|
||||
WelcomeMessage string `json:"welcome_message"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
SearchLimit int `json:"search_limit"`
|
||||
}
|
||||
|
||||
// WechatBotConfigResponse 微信公众号机器人配置响应
|
||||
type WechatBotConfigResponse struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"app_id"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
Token string `json:"token"`
|
||||
EncodingAesKey string `json:"encoding_aes_key"`
|
||||
WelcomeMessage string `json:"welcome_message"`
|
||||
AutoReplyEnabled bool `json:"auto_reply_enabled"`
|
||||
SearchLimit int `json:"search_limit"`
|
||||
}
|
||||
50
db/entity/api_access_log.go
Normal file
50
db/entity/api_access_log.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// APIAccessLog API访问日志模型
|
||||
type APIAccessLog struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
IP string `json:"ip" gorm:"size:45;not null;comment:客户端IP地址"`
|
||||
UserAgent string `json:"user_agent" gorm:"size:500;comment:用户代理"`
|
||||
Endpoint string `json:"endpoint" gorm:"size:255;not null;comment:访问的接口路径"`
|
||||
Method string `json:"method" gorm:"size:10;not null;comment:HTTP方法"`
|
||||
RequestParams string `json:"request_params" gorm:"type:text;comment:查询参数(JSON格式)"`
|
||||
ResponseStatus int `json:"response_status" gorm:"default:200;comment:响应状态码"`
|
||||
ResponseData string `json:"response_data" gorm:"type:text;comment:响应数据(JSON格式)"`
|
||||
ProcessCount int `json:"process_count" gorm:"default:0;comment:处理数量(查询结果数或添加的数量)"`
|
||||
ErrorMessage string `json:"error_message" gorm:"size:500;comment:错误消息"`
|
||||
ProcessingTime int64 `json:"processing_time" gorm:"comment:处理时间(毫秒)"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (APIAccessLog) TableName() string {
|
||||
return "api_access_logs"
|
||||
}
|
||||
|
||||
// APIAccessLogSummary API访问日志汇总统计
|
||||
type APIAccessLogSummary struct {
|
||||
TotalRequests int64 `json:"total_requests"`
|
||||
TodayRequests int64 `json:"today_requests"`
|
||||
WeekRequests int64 `json:"week_requests"`
|
||||
MonthRequests int64 `json:"month_requests"`
|
||||
ErrorRequests int64 `json:"error_requests"`
|
||||
UniqueIPs int64 `json:"unique_ips"`
|
||||
}
|
||||
|
||||
// APIAccessLogStats 按端点统计
|
||||
type APIAccessLogStats struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Method string `json:"method"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
ErrorCount int64 `json:"error_count"`
|
||||
AvgProcessTime int64 `json:"avg_process_time"`
|
||||
LastAccess time.Time `json:"last_access"`
|
||||
}
|
||||
23
db/entity/plugin_config.go
Normal file
23
db/entity/plugin_config.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// PluginConfig 插件配置实体
|
||||
type PluginConfig struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PluginName string `json:"plugin_name" gorm:"size:100;not null;uniqueIndex:idx_plugin_config_unique;comment:插件名称"`
|
||||
ConfigKey string `json:"config_key" gorm:"size:255;not null;uniqueIndex:idx_plugin_config_unique;comment:配置键"`
|
||||
ConfigValue string `json:"config_value" gorm:"type:text;comment:配置值"`
|
||||
ConfigType string `json:"config_type" gorm:"size:20;default:'string';comment:配置类型(string,int,bool,json)"`
|
||||
IsEncrypted bool `json:"is_encrypted" gorm:"default:false;comment:是否加密"`
|
||||
Description string `json:"description" gorm:"type:text;comment:配置描述"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (PluginConfig) TableName() string {
|
||||
return "plugin_configs"
|
||||
}
|
||||
23
db/entity/plugin_data.go
Normal file
23
db/entity/plugin_data.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// PluginData 插件数据实体
|
||||
type PluginData struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PluginName string `json:"plugin_name" gorm:"size:100;not null;index:idx_plugin_data_plugin;comment:插件名称"`
|
||||
DataType string `json:"data_type" gorm:"size:100;not null;index:idx_plugin_data_type;comment:数据类型"`
|
||||
DataKey string `json:"data_key" gorm:"size:255;not null;index:idx_plugin_data_key;comment:数据键"`
|
||||
DataValue string `json:"data_value" gorm:"type:text;comment:数据值"`
|
||||
Metadata string `json:"metadata" gorm:"type:json;comment:元数据"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty" gorm:"comment:过期时间"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (PluginData) TableName() string {
|
||||
return "plugin_data"
|
||||
}
|
||||
@@ -56,6 +56,24 @@ const (
|
||||
ConfigKeyTelegramProxyPort = "telegram_proxy_port"
|
||||
ConfigKeyTelegramProxyUsername = "telegram_proxy_username"
|
||||
ConfigKeyTelegramProxyPassword = "telegram_proxy_password"
|
||||
|
||||
// 微信公众号配置
|
||||
ConfigKeyWechatBotEnabled = "wechat_bot_enabled"
|
||||
ConfigKeyWechatAppId = "wechat_app_id"
|
||||
ConfigKeyWechatAppSecret = "wechat_app_secret"
|
||||
ConfigKeyWechatToken = "wechat_token"
|
||||
ConfigKeyWechatEncodingAesKey = "wechat_encoding_aes_key"
|
||||
ConfigKeyWechatWelcomeMessage = "wechat_welcome_message"
|
||||
ConfigKeyWechatAutoReplyEnabled = "wechat_auto_reply_enabled"
|
||||
ConfigKeyWechatSearchLimit = "wechat_search_limit"
|
||||
|
||||
// 界面配置
|
||||
ConfigKeyEnableAnnouncements = "enable_announcements"
|
||||
ConfigKeyAnnouncements = "announcements"
|
||||
ConfigKeyEnableFloatButtons = "enable_float_buttons"
|
||||
ConfigKeyWechatSearchImage = "wechat_search_image"
|
||||
ConfigKeyTelegramQrImage = "telegram_qr_image"
|
||||
ConfigKeyQrCodeStyle = "qr_code_style"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
@@ -126,6 +144,24 @@ const (
|
||||
ConfigResponseFieldTelegramProxyPort = "telegram_proxy_port"
|
||||
ConfigResponseFieldTelegramProxyUsername = "telegram_proxy_username"
|
||||
ConfigResponseFieldTelegramProxyPassword = "telegram_proxy_password"
|
||||
|
||||
// 微信公众号配置字段
|
||||
ConfigResponseFieldWechatBotEnabled = "wechat_bot_enabled"
|
||||
ConfigResponseFieldWechatAppId = "wechat_app_id"
|
||||
ConfigResponseFieldWechatAppSecret = "wechat_app_secret"
|
||||
ConfigResponseFieldWechatToken = "wechat_token"
|
||||
ConfigResponseFieldWechatEncodingAesKey = "wechat_encoding_aes_key"
|
||||
ConfigResponseFieldWechatWelcomeMessage = "wechat_welcome_message"
|
||||
ConfigResponseFieldWechatAutoReplyEnabled = "wechat_auto_reply_enabled"
|
||||
ConfigResponseFieldWechatSearchLimit = "wechat_search_limit"
|
||||
|
||||
// 界面配置字段
|
||||
ConfigResponseFieldEnableAnnouncements = "enable_announcements"
|
||||
ConfigResponseFieldAnnouncements = "announcements"
|
||||
ConfigResponseFieldEnableFloatButtons = "enable_float_buttons"
|
||||
ConfigResponseFieldWechatSearchImage = "wechat_search_image"
|
||||
ConfigResponseFieldTelegramQrImage = "telegram_qr_image"
|
||||
ConfigResponseFieldQrCodeStyle = "qr_code_style"
|
||||
)
|
||||
|
||||
// ConfigDefaultValue 配置默认值常量
|
||||
@@ -183,4 +219,22 @@ const (
|
||||
ConfigDefaultTelegramProxyPort = "8080"
|
||||
ConfigDefaultTelegramProxyUsername = ""
|
||||
ConfigDefaultTelegramProxyPassword = ""
|
||||
|
||||
// 微信公众号配置默认值
|
||||
ConfigDefaultWechatBotEnabled = "false"
|
||||
ConfigDefaultWechatAppId = ""
|
||||
ConfigDefaultWechatAppSecret = ""
|
||||
ConfigDefaultWechatToken = ""
|
||||
ConfigDefaultWechatEncodingAesKey = ""
|
||||
ConfigDefaultWechatWelcomeMessage = "欢迎关注老九网盘资源库!发送关键词即可搜索资源。"
|
||||
ConfigDefaultWechatAutoReplyEnabled = "true"
|
||||
ConfigDefaultWechatSearchLimit = "5"
|
||||
|
||||
// 界面配置默认值
|
||||
ConfigDefaultEnableAnnouncements = "false"
|
||||
ConfigDefaultAnnouncements = ""
|
||||
ConfigDefaultEnableFloatButtons = "false"
|
||||
ConfigDefaultWechatSearchImage = ""
|
||||
ConfigDefaultTelegramQrImage = ""
|
||||
ConfigDefaultQrCodeStyle = "Plain"
|
||||
)
|
||||
|
||||
@@ -36,6 +36,10 @@ type TelegramChannel struct {
|
||||
Token string `json:"token" gorm:"size:255;comment:访问令牌"`
|
||||
ApiType string `json:"api_type" gorm:"size:50;comment:API类型"`
|
||||
IsPushSavedInfo bool `json:"is_push_saved_info" gorm:"default:false;comment:是否只推送已转存资源"`
|
||||
|
||||
// 资源策略和时间限制配置
|
||||
ResourceStrategy string `json:"resource_strategy" gorm:"size:20;default:'random';comment:资源策略:latest-最新优先,transferred-已转存优先,random-纯随机"`
|
||||
TimeLimit string `json:"time_limit" gorm:"size:20;default:'none';comment:时间限制:none-无限制,week-一周内,month-一月内"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
|
||||
169
db/repo/api_access_log_repository.go
Normal file
169
db/repo/api_access_log_repository.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// APIAccessLogRepository API访问日志Repository接口
|
||||
type APIAccessLogRepository interface {
|
||||
BaseRepository[entity.APIAccessLog]
|
||||
RecordAccess(ip, userAgent, endpoint, method string, requestParams interface{}, responseStatus int, responseData interface{}, processCount int, errorMessage string, processingTime int64) error
|
||||
GetSummary() (*entity.APIAccessLogSummary, error)
|
||||
GetStatsByEndpoint() ([]entity.APIAccessLogStats, error)
|
||||
FindWithFilters(page, limit int, startDate, endDate *time.Time, endpoint, ip string) ([]entity.APIAccessLog, int64, error)
|
||||
ClearOldLogs(days int) error
|
||||
}
|
||||
|
||||
// APIAccessLogRepositoryImpl API访问日志Repository实现
|
||||
type APIAccessLogRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.APIAccessLog]
|
||||
}
|
||||
|
||||
// NewAPIAccessLogRepository 创建API访问日志Repository
|
||||
func NewAPIAccessLogRepository(db *gorm.DB) APIAccessLogRepository {
|
||||
return &APIAccessLogRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.APIAccessLog]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// RecordAccess 记录API访问
|
||||
func (r *APIAccessLogRepositoryImpl) RecordAccess(ip, userAgent, endpoint, method string, requestParams interface{}, responseStatus int, responseData interface{}, processCount int, errorMessage string, processingTime int64) error {
|
||||
log := entity.APIAccessLog{
|
||||
IP: ip,
|
||||
UserAgent: userAgent,
|
||||
Endpoint: endpoint,
|
||||
Method: method,
|
||||
ResponseStatus: responseStatus,
|
||||
ProcessCount: processCount,
|
||||
ErrorMessage: errorMessage,
|
||||
ProcessingTime: processingTime,
|
||||
}
|
||||
|
||||
// 序列化请求参数
|
||||
if requestParams != nil {
|
||||
if paramsJSON, err := json.Marshal(requestParams); err == nil {
|
||||
log.RequestParams = string(paramsJSON)
|
||||
}
|
||||
}
|
||||
|
||||
// 序列化响应数据(限制大小,避免存储大量数据)
|
||||
if responseData != nil {
|
||||
if dataJSON, err := json.Marshal(responseData); err == nil {
|
||||
// 限制响应数据长度,避免存储过多数据
|
||||
dataStr := string(dataJSON)
|
||||
if len(dataStr) > 2000 {
|
||||
dataStr = dataStr[:2000] + "..."
|
||||
}
|
||||
log.ResponseData = dataStr
|
||||
}
|
||||
}
|
||||
|
||||
return r.db.Create(&log).Error
|
||||
}
|
||||
|
||||
// GetSummary 获取访问日志汇总
|
||||
func (r *APIAccessLogRepositoryImpl) GetSummary() (*entity.APIAccessLogSummary, error) {
|
||||
var summary entity.APIAccessLogSummary
|
||||
now := utils.GetCurrentTime()
|
||||
todayStr := now.Format(utils.TimeFormatDate)
|
||||
weekStart := now.AddDate(0, 0, -int(now.Weekday())+1).Format(utils.TimeFormatDate)
|
||||
monthStart := now.Format("2006-01") + "-01"
|
||||
|
||||
// 总请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Count(&summary.TotalRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 今日请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("DATE(created_at) = ?", todayStr).Count(&summary.TodayRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 本周请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("created_at >= ?", weekStart).Count(&summary.WeekRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 本月请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("created_at >= ?", monthStart).Count(&summary.MonthRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 错误请求数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Where("response_status >= 400").Count(&summary.ErrorRequests).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 唯一IP数
|
||||
if err := r.db.Model(&entity.APIAccessLog{}).Distinct("ip").Count(&summary.UniqueIPs).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
// GetStatsByEndpoint 按端点获取统计
|
||||
func (r *APIAccessLogRepositoryImpl) GetStatsByEndpoint() ([]entity.APIAccessLogStats, error) {
|
||||
var stats []entity.APIAccessLogStats
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
endpoint,
|
||||
method,
|
||||
COUNT(*) as request_count,
|
||||
SUM(CASE WHEN response_status >= 400 THEN 1 ELSE 0 END) as error_count,
|
||||
AVG(processing_time) as avg_process_time,
|
||||
MAX(created_at) as last_access
|
||||
FROM api_access_logs
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY endpoint, method
|
||||
ORDER BY request_count DESC
|
||||
`
|
||||
|
||||
err := r.db.Raw(query).Scan(&stats).Error
|
||||
return stats, err
|
||||
}
|
||||
|
||||
// FindWithFilters 带过滤条件的分页查找访问日志
|
||||
func (r *APIAccessLogRepositoryImpl) FindWithFilters(page, limit int, startDate, endDate *time.Time, endpoint, ip string) ([]entity.APIAccessLog, int64, error) {
|
||||
var logs []entity.APIAccessLog
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
query := r.db.Model(&entity.APIAccessLog{})
|
||||
|
||||
// 添加过滤条件
|
||||
if startDate != nil {
|
||||
query = query.Where("created_at >= ?", *startDate)
|
||||
}
|
||||
if endDate != nil {
|
||||
query = query.Where("created_at <= ?", *endDate)
|
||||
}
|
||||
if endpoint != "" {
|
||||
query = query.Where("endpoint LIKE ?", "%"+endpoint+"%")
|
||||
}
|
||||
if ip != "" {
|
||||
query = query.Where("ip = ?", ip)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据,按创建时间倒序排列
|
||||
err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
// ClearOldLogs 清理旧日志
|
||||
func (r *APIAccessLogRepositoryImpl) ClearOldLogs(days int) error {
|
||||
cutoffDate := utils.GetCurrentTime().AddDate(0, 0, -days)
|
||||
return r.db.Where("created_at < ?", cutoffDate).Delete(&entity.APIAccessLog{}).Error
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -66,20 +69,28 @@ func (r *CksRepositoryImpl) FindAll() ([]entity.Cks, error) {
|
||||
|
||||
// FindByID 根据ID查找Cks,预加载Pan关联数据
|
||||
func (r *CksRepositoryImpl) FindByID(id uint) (*entity.Cks, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var cks entity.Cks
|
||||
err := r.db.Preload("Pan").First(&cks, id).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("FindByID失败: ID=%d, 错误=%v, 查询耗时=%v", id, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("FindByID成功: ID=%d, 查询耗时=%v", id, queryDuration)
|
||||
return &cks, nil
|
||||
}
|
||||
|
||||
func (r *CksRepositoryImpl) FindByIds(ids []uint) ([]*entity.Cks, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var cks []*entity.Cks
|
||||
err := r.db.Preload("Pan").Where("id IN ?", ids).Find(&cks).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("FindByIds失败: IDs数量=%d, 错误=%v, 查询耗时=%v", len(ids), err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("FindByIds成功: 找到%d个账号,查询耗时=%v", len(cks), queryDuration)
|
||||
return cks, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ type RepositoryManager struct {
|
||||
TaskItemRepository TaskItemRepository
|
||||
FileRepository FileRepository
|
||||
TelegramChannelRepository TelegramChannelRepository
|
||||
APIAccessLogRepository APIAccessLogRepository
|
||||
PluginDataRepository PluginDataRepository
|
||||
PluginConfigRepository PluginConfigRepository
|
||||
}
|
||||
|
||||
// NewRepositoryManager 创建Repository管理器
|
||||
@@ -41,5 +44,8 @@ func NewRepositoryManager(db *gorm.DB) *RepositoryManager {
|
||||
TaskItemRepository: NewTaskItemRepository(db),
|
||||
FileRepository: NewFileRepository(db),
|
||||
TelegramChannelRepository: NewTelegramChannelRepository(db),
|
||||
APIAccessLogRepository: NewAPIAccessLogRepository(db),
|
||||
PluginDataRepository: NewPluginDataRepository(db),
|
||||
PluginConfigRepository: NewPluginConfigRepository(db),
|
||||
}
|
||||
}
|
||||
|
||||
114
db/repo/pagination.go
Normal file
114
db/repo/pagination.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PaginationResult 分页查询结果
|
||||
type PaginationResult[T any] struct {
|
||||
Data []T `json:"data"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// PaginationOptions 分页查询选项
|
||||
type PaginationOptions struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"` // asc or desc
|
||||
Preloads []string `json:"preloads"` // 需要预加载的关联
|
||||
Filters map[string]interface{} `json:"filters"` // 过滤条件
|
||||
}
|
||||
|
||||
// DefaultPaginationOptions 默认分页选项
|
||||
func DefaultPaginationOptions() *PaginationOptions {
|
||||
return &PaginationOptions{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
OrderBy: "id",
|
||||
OrderDir: "desc",
|
||||
Preloads: []string{},
|
||||
Filters: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// PaginatedQuery 通用分页查询函数
|
||||
func PaginatedQuery[T any](db *gorm.DB, options *PaginationOptions) (*PaginationResult[T], error) {
|
||||
// 验证分页参数
|
||||
if options.Page < 1 {
|
||||
options.Page = 1
|
||||
}
|
||||
if options.PageSize < 1 || options.PageSize > 1000 {
|
||||
options.PageSize = 20
|
||||
}
|
||||
|
||||
// 应用预加载
|
||||
query := db.Model(new(T))
|
||||
for _, preload := range options.Preloads {
|
||||
query = query.Preload(preload)
|
||||
}
|
||||
|
||||
// 应用过滤条件
|
||||
for key, value := range options.Filters {
|
||||
// 处理特殊过滤条件
|
||||
switch key {
|
||||
case "search":
|
||||
// 搜索条件需要特殊处理
|
||||
if searchStr, ok := value.(string); ok && searchStr != "" {
|
||||
query = query.Where("title ILIKE ? OR description ILIKE ?", "%"+searchStr+"%", "%"+searchStr+"%")
|
||||
}
|
||||
case "category_id":
|
||||
if categoryID, ok := value.(uint); ok {
|
||||
query = query.Where("category_id = ?", categoryID)
|
||||
}
|
||||
case "pan_id":
|
||||
if panID, ok := value.(uint); ok {
|
||||
query = query.Where("pan_id = ?", panID)
|
||||
}
|
||||
case "is_valid":
|
||||
if isValid, ok := value.(bool); ok {
|
||||
query = query.Where("is_valid = ?", isValid)
|
||||
}
|
||||
case "is_public":
|
||||
if isPublic, ok := value.(bool); ok {
|
||||
query = query.Where("is_public = ?", isPublic)
|
||||
}
|
||||
default:
|
||||
// 通用过滤条件
|
||||
query = query.Where(key+" = ?", value)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
orderClause := options.OrderBy + " " + options.OrderDir
|
||||
query = query.Order(orderClause)
|
||||
|
||||
// 计算偏移量
|
||||
offset := (options.Page - 1) * options.PageSize
|
||||
|
||||
// 获取总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
var data []T
|
||||
if err := query.Offset(offset).Limit(options.PageSize).Find(&data).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
totalPages := int((total + int64(options.PageSize) - 1) / int64(options.PageSize))
|
||||
|
||||
return &PaginationResult[T]{
|
||||
Data: data,
|
||||
Total: total,
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
81
db/repo/plugin_config_repository.go
Normal file
81
db/repo/plugin_config_repository.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PluginConfigRepository 插件配置Repository接口
|
||||
type PluginConfigRepository interface {
|
||||
BaseRepository[entity.PluginConfig]
|
||||
FindByPluginAndKey(pluginName, key string) (*entity.PluginConfig, error)
|
||||
FindByPlugin(pluginName string) ([]entity.PluginConfig, error)
|
||||
Upsert(pluginName, key, value, configType string, isEncrypted bool, description string) error
|
||||
DeleteByPluginAndKey(pluginName, key string) error
|
||||
DeleteByPlugin(pluginName string) error
|
||||
}
|
||||
|
||||
// PluginConfigRepositoryImpl 插件配置Repository实现
|
||||
type PluginConfigRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.PluginConfig]
|
||||
}
|
||||
|
||||
// NewPluginConfigRepository 创建插件配置Repository
|
||||
func NewPluginConfigRepository(db *gorm.DB) PluginConfigRepository {
|
||||
return &PluginConfigRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.PluginConfig]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// FindByPluginAndKey 根据插件名称和键查找配置
|
||||
func (r *PluginConfigRepositoryImpl) FindByPluginAndKey(pluginName, key string) (*entity.PluginConfig, error) {
|
||||
var config entity.PluginConfig
|
||||
err := r.db.Where("plugin_name = ? AND config_key = ?", pluginName, key).First(&config).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// FindByPlugin 根据插件名称查找所有配置
|
||||
func (r *PluginConfigRepositoryImpl) FindByPlugin(pluginName string) ([]entity.PluginConfig, error) {
|
||||
var configs []entity.PluginConfig
|
||||
err := r.db.Where("plugin_name = ?", pluginName).Find(&configs).Error
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// Upsert 创建或更新插件配置
|
||||
func (r *PluginConfigRepositoryImpl) Upsert(pluginName, key, value, configType string, isEncrypted bool, description string) error {
|
||||
var existingConfig entity.PluginConfig
|
||||
err := r.db.Where("plugin_name = ? AND config_key = ?", pluginName, key).First(&existingConfig).Error
|
||||
|
||||
if err != nil {
|
||||
// 如果不存在,则创建
|
||||
newConfig := entity.PluginConfig{
|
||||
PluginName: pluginName,
|
||||
ConfigKey: key,
|
||||
ConfigValue: value,
|
||||
ConfigType: configType,
|
||||
IsEncrypted: isEncrypted,
|
||||
Description: description,
|
||||
}
|
||||
return r.db.Create(&newConfig).Error
|
||||
} else {
|
||||
// 如果存在,则更新
|
||||
existingConfig.ConfigValue = value
|
||||
existingConfig.ConfigType = configType
|
||||
existingConfig.IsEncrypted = isEncrypted
|
||||
existingConfig.Description = description
|
||||
return r.db.Save(&existingConfig).Error
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteByPluginAndKey 根据插件名称和键删除配置
|
||||
func (r *PluginConfigRepositoryImpl) DeleteByPluginAndKey(pluginName, key string) error {
|
||||
return r.db.Where("plugin_name = ? AND config_key = ?", pluginName, key).Delete(&entity.PluginConfig{}).Error
|
||||
}
|
||||
|
||||
// DeleteByPlugin 根据插件名称删除所有配置
|
||||
func (r *PluginConfigRepositoryImpl) DeleteByPlugin(pluginName string) error {
|
||||
return r.db.Where("plugin_name = ?", pluginName).Delete(&entity.PluginConfig{}).Error
|
||||
}
|
||||
61
db/repo/plugin_data_repository.go
Normal file
61
db/repo/plugin_data_repository.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// PluginDataRepository 插件数据Repository接口
|
||||
type PluginDataRepository interface {
|
||||
BaseRepository[entity.PluginData]
|
||||
FindByPluginAndKey(pluginName, dataType, key string) (*entity.PluginData, error)
|
||||
FindByPluginAndType(pluginName, dataType string) ([]entity.PluginData, error)
|
||||
DeleteByPluginAndKey(pluginName, dataType, key string) error
|
||||
DeleteByPluginAndType(pluginName, dataType string) error
|
||||
DeleteExpired() (int64, error)
|
||||
}
|
||||
|
||||
// PluginDataRepositoryImpl 插件数据Repository实现
|
||||
type PluginDataRepositoryImpl struct {
|
||||
BaseRepositoryImpl[entity.PluginData]
|
||||
}
|
||||
|
||||
// NewPluginDataRepository 创建插件数据Repository
|
||||
func NewPluginDataRepository(db *gorm.DB) PluginDataRepository {
|
||||
return &PluginDataRepositoryImpl{
|
||||
BaseRepositoryImpl: BaseRepositoryImpl[entity.PluginData]{db: db},
|
||||
}
|
||||
}
|
||||
|
||||
// FindByPluginAndKey 根据插件名称、数据类型和键查找数据
|
||||
func (r *PluginDataRepositoryImpl) FindByPluginAndKey(pluginName, dataType, key string) (*entity.PluginData, error) {
|
||||
var data entity.PluginData
|
||||
err := r.db.Where("plugin_name = ? AND data_type = ? AND data_key = ?", pluginName, dataType, key).First(&data).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// FindByPluginAndType 根据插件名称和数据类型查找数据
|
||||
func (r *PluginDataRepositoryImpl) FindByPluginAndType(pluginName, dataType string) ([]entity.PluginData, error) {
|
||||
var data []entity.PluginData
|
||||
err := r.db.Where("plugin_name = ? AND data_type = ?", pluginName, dataType).Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
// DeleteByPluginAndKey 根据插件名称、数据类型和键删除数据
|
||||
func (r *PluginDataRepositoryImpl) DeleteByPluginAndKey(pluginName, dataType, key string) error {
|
||||
return r.db.Where("plugin_name = ? AND data_type = ? AND data_key = ?", pluginName, dataType, key).Delete(&entity.PluginData{}).Error
|
||||
}
|
||||
|
||||
// DeleteByPluginAndType 根据插件名称和数据类型删除数据
|
||||
func (r *PluginDataRepositoryImpl) DeleteByPluginAndType(pluginName, dataType string) error {
|
||||
return r.db.Where("plugin_name = ? AND data_type = ?", pluginName, dataType).Delete(&entity.PluginData{}).Error
|
||||
}
|
||||
|
||||
// DeleteExpired 删除过期数据
|
||||
func (r *PluginDataRepositoryImpl) DeleteExpired() (int64, error) {
|
||||
result := r.db.Where("expires_at IS NOT NULL AND expires_at < NOW()").Delete(&entity.PluginData{})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -68,38 +69,21 @@ func (r *ResourceRepositoryImpl) FindWithRelations() ([]entity.Resource, error)
|
||||
|
||||
// FindWithRelationsPaginated 分页查找包含关联关系的资源
|
||||
func (r *ResourceRepositoryImpl) FindWithRelationsPaginated(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 优化查询:只预加载必要的关联,并添加排序
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Order("updated_at DESC") // 按更新时间倒序,显示最新内容
|
||||
|
||||
// 获取总数(使用缓存键)
|
||||
cacheKey := fmt.Sprintf("resources_total_%d_%d", page, limit)
|
||||
if cached, exists := r.cache[cacheKey]; exists {
|
||||
if totalCached, ok := cached.(int64); ok {
|
||||
total = totalCached
|
||||
}
|
||||
} else {
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// 缓存总数(5分钟)
|
||||
r.cache[cacheKey] = total
|
||||
go func() {
|
||||
time.Sleep(5 * time.Minute)
|
||||
delete(r.cache, cacheKey)
|
||||
}()
|
||||
// 使用新的分页查询功能
|
||||
options := &PaginationOptions{
|
||||
Page: page,
|
||||
PageSize: limit,
|
||||
OrderBy: "updated_at",
|
||||
OrderDir: "desc",
|
||||
Preloads: []string{"Category", "Pan"},
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
result, err := PaginatedQuery[entity.Resource](r.db, options)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return result.Data, result.Total, nil
|
||||
}
|
||||
|
||||
// FindByCategoryID 根据分类ID查找
|
||||
@@ -218,6 +202,7 @@ func (r *ResourceRepositoryImpl) SearchByPanID(query string, panID uint, page, l
|
||||
|
||||
// SearchWithFilters 根据参数进行搜索
|
||||
func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}) ([]entity.Resource, int64, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
@@ -335,8 +320,11 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
// 获取分页数据,按更新时间倒序
|
||||
queryStart := utils.GetCurrentTime()
|
||||
err := db.Order("updated_at DESC").Offset(offset).Limit(pageSize).Find(&resources).Error
|
||||
fmt.Printf("查询结果: 总数=%d, 当前页数据量=%d, pageSize=%d\n", total, len(resources), pageSize)
|
||||
queryDuration := time.Since(queryStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("SearchWithFilters完成: 总数=%d, 当前页数据量=%d, 查询耗时=%v, 总耗时=%v", total, len(resources), queryDuration, totalDuration)
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
@@ -469,11 +457,15 @@ func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime t
|
||||
|
||||
// GetByURL 根据URL获取资源
|
||||
func (r *ResourceRepositoryImpl) GetByURL(url string) (*entity.Resource, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var resource entity.Resource
|
||||
err := r.db.Where("url = ?", url).First(&resource).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("GetByURL失败: URL=%s, 错误=%v, 查询耗时=%v", url, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("GetByURL成功: URL=%s, 查询耗时=%v", url, queryDuration)
|
||||
return &resource, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package repo
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
@@ -100,8 +101,11 @@ func (r *SystemConfigRepositoryImpl) UpsertConfigs(configs []entity.SystemConfig
|
||||
|
||||
// GetOrCreateDefault 获取配置或创建默认配置
|
||||
func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
configs, err := r.FindAll()
|
||||
initialQueryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("获取所有系统配置失败: %v,耗时: %v", err, initialQueryDuration)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -133,13 +137,24 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyEnableAnnouncements, Value: entity.ConfigDefaultEnableAnnouncements, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyAnnouncements, Value: entity.ConfigDefaultAnnouncements, Type: entity.ConfigTypeJSON},
|
||||
{Key: entity.ConfigKeyEnableFloatButtons, Value: entity.ConfigDefaultEnableFloatButtons, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyWechatSearchImage, Value: entity.ConfigDefaultWechatSearchImage, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyTelegramQrImage, Value: entity.ConfigDefaultTelegramQrImage, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyQrCodeStyle, Value: entity.ConfigDefaultQrCodeStyle, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
createStart := utils.GetCurrentTime()
|
||||
err = r.UpsertConfigs(defaultConfigs)
|
||||
createDuration := time.Since(createStart)
|
||||
if err != nil {
|
||||
utils.Error("创建默认系统配置失败: %v,耗时: %v", err, createDuration)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Info("创建默认系统配置成功,数量: %d,总耗时: %v", len(defaultConfigs), totalDuration)
|
||||
return defaultConfigs, nil
|
||||
}
|
||||
|
||||
@@ -169,6 +184,11 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
entity.ConfigKeyMeilisearchPort: {Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchMasterKey: {Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchIndexName: {Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyEnableAnnouncements: {Key: entity.ConfigKeyEnableAnnouncements, Value: entity.ConfigDefaultEnableAnnouncements, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyAnnouncements: {Key: entity.ConfigKeyAnnouncements, Value: entity.ConfigDefaultAnnouncements, Type: entity.ConfigTypeJSON},
|
||||
entity.ConfigKeyEnableFloatButtons: {Key: entity.ConfigKeyEnableFloatButtons, Value: entity.ConfigDefaultEnableFloatButtons, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyWechatSearchImage: {Key: entity.ConfigKeyWechatSearchImage, Value: entity.ConfigDefaultWechatSearchImage, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyTelegramQrImage: {Key: entity.ConfigKeyTelegramQrImage, Value: entity.ConfigDefaultTelegramQrImage, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
// 检查现有配置中是否有缺失的配置项
|
||||
@@ -187,17 +207,24 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
|
||||
// 如果有缺失的配置项,则添加它们
|
||||
if len(missingConfigs) > 0 {
|
||||
upsertStart := utils.GetCurrentTime()
|
||||
err = r.UpsertConfigs(missingConfigs)
|
||||
upsertDuration := time.Since(upsertStart)
|
||||
if err != nil {
|
||||
utils.Error("添加缺失的系统配置失败: %v,耗时: %v", err, upsertDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("添加缺失的系统配置完成,数量: %d,耗时: %v", len(missingConfigs), upsertDuration)
|
||||
// 重新获取所有配置
|
||||
configs, err = r.FindAll()
|
||||
if err != nil {
|
||||
utils.Error("重新获取所有系统配置失败: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("GetOrCreateDefault完成,总数: %d,总耗时: %v", len(configs), totalDuration)
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -58,8 +61,15 @@ func (r *TaskItemRepositoryImpl) DeleteByTaskID(taskID uint) error {
|
||||
|
||||
// GetByTaskIDAndStatus 根据任务ID和状态获取任务项
|
||||
func (r *TaskItemRepositoryImpl) GetByTaskIDAndStatus(taskID uint, status string) ([]*entity.TaskItem, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var items []*entity.TaskItem
|
||||
err := r.db.Where("task_id = ? AND status = ?", taskID, status).Order("id ASC").Find(&items).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("GetByTaskIDAndStatus失败: 任务ID=%d, 状态=%s, 错误=%v, 查询耗时=%v", taskID, status, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("GetByTaskIDAndStatus成功: 任务ID=%d, 状态=%s, 数量=%d, 查询耗时=%v", taskID, status, len(items), queryDuration)
|
||||
return items, err
|
||||
}
|
||||
|
||||
@@ -93,19 +103,36 @@ func (r *TaskItemRepositoryImpl) GetListByTaskID(taskID uint, page, pageSize int
|
||||
|
||||
// UpdateStatus 更新任务项状态
|
||||
func (r *TaskItemRepositoryImpl) UpdateStatus(id uint, status string) error {
|
||||
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Update("status", status).Error
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatus失败: ID=%d, 状态=%s, 错误=%v, 更新耗时=%v", id, status, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatus成功: ID=%d, 状态=%s, 更新耗时=%v", id, status, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatusAndOutput 更新任务项状态和输出数据
|
||||
func (r *TaskItemRepositoryImpl) UpdateStatusAndOutput(id uint, status, outputData string) error {
|
||||
return r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.TaskItem{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"output_data": outputData,
|
||||
}).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndOutput失败: ID=%d, 状态=%s, 错误=%v, 更新耗时=%v", id, status, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndOutput成功: ID=%d, 状态=%s, 更新耗时=%v", id, status, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetStatsByTaskID 获取任务项统计信息
|
||||
func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var results []struct {
|
||||
Status string
|
||||
Count int
|
||||
@@ -117,7 +144,9 @@ func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int,
|
||||
Group("status").
|
||||
Find(&results).Error
|
||||
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("GetStatsByTaskID失败: 任务ID=%d, 错误=%v, 查询耗时=%v", taskID, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -134,12 +163,22 @@ func (r *TaskItemRepositoryImpl) GetStatsByTaskID(taskID uint) (map[string]int,
|
||||
stats["total"] += result.Count
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("GetStatsByTaskID成功: 任务ID=%d, 统计信息=%v, 查询耗时=%v, 总耗时=%v", taskID, stats, queryDuration, totalDuration)
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// ResetProcessingItems 重置处理中的任务项为pending状态
|
||||
func (r *TaskItemRepositoryImpl) ResetProcessingItems(taskID uint) error {
|
||||
return r.db.Model(&entity.TaskItem{}).
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.TaskItem{}).
|
||||
Where("task_id = ? AND status = ?", taskID, "processing").
|
||||
Update("status", "pending").Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("ResetProcessingItems失败: 任务ID=%d, 错误=%v, 更新耗时=%v", taskID, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("ResetProcessingItems成功: 任务ID=%d, 更新耗时=%v", taskID, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -35,11 +36,15 @@ func NewTaskRepository(db *gorm.DB) TaskRepository {
|
||||
|
||||
// GetByID 根据ID获取任务
|
||||
func (r *TaskRepositoryImpl) GetByID(id uint) (*entity.Task, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var task entity.Task
|
||||
err := r.db.First(&task, id).Error
|
||||
queryDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Debug("GetByID失败: ID=%d, 错误=%v, 查询耗时=%v", id, err, queryDuration)
|
||||
return nil, err
|
||||
}
|
||||
utils.Debug("GetByID成功: ID=%d, 查询耗时=%v", id, queryDuration)
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
@@ -55,6 +60,7 @@ func (r *TaskRepositoryImpl) Delete(id uint) error {
|
||||
|
||||
// GetList 获取任务列表
|
||||
func (r *TaskRepositoryImpl) GetList(page, pageSize int, taskType, status string) ([]*entity.Task, int64, error) {
|
||||
startTime := utils.GetCurrentTime()
|
||||
var tasks []*entity.Task
|
||||
var total int64
|
||||
|
||||
@@ -69,84 +75,171 @@ func (r *TaskRepositoryImpl) GetList(page, pageSize int, taskType, status string
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
countStart := utils.GetCurrentTime()
|
||||
err := query.Count(&total).Error
|
||||
countDuration := time.Since(countStart)
|
||||
if err != nil {
|
||||
utils.Error("GetList获取总数失败: 错误=%v, 查询耗时=%v", err, countDuration)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * pageSize
|
||||
queryStart := utils.GetCurrentTime()
|
||||
err = query.Offset(offset).Limit(pageSize).Order("created_at DESC").Find(&tasks).Error
|
||||
queryDuration := time.Since(queryStart)
|
||||
if err != nil {
|
||||
utils.Error("GetList查询失败: 错误=%v, 查询耗时=%v", err, queryDuration)
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
totalDuration := time.Since(startTime)
|
||||
utils.Debug("GetList完成: 任务类型=%s, 状态=%s, 页码=%d, 页面大小=%d, 总数=%d, 结果数=%d, 总耗时=%v", taskType, status, page, pageSize, total, len(tasks), totalDuration)
|
||||
return tasks, total, nil
|
||||
}
|
||||
|
||||
// UpdateStatus 更新任务状态
|
||||
func (r *TaskRepositoryImpl) UpdateStatus(id uint, status string) error {
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatus失败: ID=%d, 状态=%s, 错误=%v, 更新耗时=%v", id, status, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatus成功: ID=%d, 状态=%s, 更新耗时=%v", id, status, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProgress 更新任务进度
|
||||
func (r *TaskRepositoryImpl) UpdateProgress(id uint, progress float64, progressData string) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
// 检查progress和progress_data字段是否存在
|
||||
var count int64
|
||||
err := r.db.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'tasks' AND column_name = 'progress'").Count(&count).Error
|
||||
if err != nil || count == 0 {
|
||||
// 如果检查失败或字段不存在,只更新processed_items等现有字段
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"processed_items": progress, // 使用progress作为processed_items的近似值
|
||||
}).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateProgress失败(字段不存在): ID=%d, 进度=%f, 错误=%v, 更新耗时=%v, 总耗时=%v", id, progress, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateProgress成功(字段不存在): ID=%d, 进度=%f, 更新耗时=%v, 总耗时=%v", id, progress, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 字段存在,正常更新
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err = r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"progress": progress,
|
||||
"progress_data": progressData,
|
||||
}).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateProgress失败: ID=%d, 进度=%f, 错误=%v, 更新耗时=%v, 总耗时=%v", id, progress, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateProgress成功: ID=%d, 进度=%f, 更新耗时=%v, 总耗时=%v", id, progress, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStatusAndMessage 更新任务状态和消息
|
||||
func (r *TaskRepositoryImpl) UpdateStatusAndMessage(id uint, status, message string) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
// 检查message字段是否存在
|
||||
var count int64
|
||||
err := r.db.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'tasks' AND column_name = 'message'").Count(&count).Error
|
||||
if err != nil {
|
||||
// 如果检查失败,只更新状态
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndMessage失败(检查失败): ID=%d, 状态=%s, 错误=%v, 更新耗时=%v, 总耗时=%v", id, status, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndMessage成功(检查失败): ID=%d, 状态=%s, 更新耗时=%v, 总耗时=%v", id, status, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
// message字段存在,更新状态和消息
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"status": status,
|
||||
"message": message,
|
||||
}).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndMessage失败(字段存在): ID=%d, 状态=%s, 错误=%v, 更新耗时=%v, 总耗时=%v", id, status, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndMessage成功(字段存在): ID=%d, 状态=%s, 更新耗时=%v, 总耗时=%v", id, status, updateDuration, totalDuration)
|
||||
return nil
|
||||
} else {
|
||||
// message字段不存在,只更新状态
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateStart := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("status", status).Error
|
||||
updateDuration := time.Since(updateStart)
|
||||
totalDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStatusAndMessage失败(字段不存在): ID=%d, 状态=%s, 错误=%v, 更新耗时=%v, 总耗时=%v", id, status, err, updateDuration, totalDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStatusAndMessage成功(字段不存在): ID=%d, 状态=%s, 更新耗时=%v, 总耗时=%v", id, status, updateDuration, totalDuration)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateTaskStats 更新任务统计信息
|
||||
func (r *TaskRepositoryImpl) UpdateTaskStats(id uint, processed, success, failed int) error {
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
startTime := utils.GetCurrentTime()
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"processed_items": processed,
|
||||
"success_items": success,
|
||||
"failed_items": failed,
|
||||
}).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateTaskStats失败: ID=%d, 处理数=%d, 成功数=%d, 失败数=%d, 错误=%v, 更新耗时=%v", id, processed, success, failed, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateTaskStats成功: ID=%d, 处理数=%d, 成功数=%d, 失败数=%d, 更新耗时=%v", id, processed, success, failed, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateStartedAt 更新任务开始时间
|
||||
func (r *TaskRepositoryImpl) UpdateStartedAt(id uint) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
now := time.Now()
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("started_at", now).Error
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("started_at", now).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateStartedAt失败: ID=%d, 错误=%v, 更新耗时=%v", id, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateStartedAt成功: ID=%d, 更新耗时=%v", id, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCompletedAt 更新任务完成时间
|
||||
func (r *TaskRepositoryImpl) UpdateCompletedAt(id uint) error {
|
||||
startTime := utils.GetCurrentTime()
|
||||
now := time.Now()
|
||||
return r.db.Model(&entity.Task{}).Where("id = ?", id).Update("completed_at", now).Error
|
||||
err := r.db.Model(&entity.Task{}).Where("id = ?", id).Update("completed_at", now).Error
|
||||
updateDuration := time.Since(startTime)
|
||||
if err != nil {
|
||||
utils.Error("UpdateCompletedAt失败: ID=%d, 错误=%v, 更新耗时=%v", id, err, updateDuration)
|
||||
return err
|
||||
}
|
||||
utils.Debug("UpdateCompletedAt成功: ID=%d, 更新耗时=%v", id, updateDuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ type TelegramChannelRepository interface {
|
||||
UpdateLastPushAt(id uint, lastPushAt time.Time) error
|
||||
FindDueForPush() ([]entity.TelegramChannel, error)
|
||||
CleanupDuplicateChannels() error
|
||||
FindActiveChannelsByTypes(chatTypes []string) ([]entity.TelegramChannel, error)
|
||||
}
|
||||
|
||||
type TelegramChannelRepositoryImpl struct {
|
||||
@@ -80,6 +81,13 @@ func (r *TelegramChannelRepositoryImpl) FindByChatType(chatType string) ([]entit
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// FindActiveChannelsByTypes 根据多个类型查找活跃频道/群组
|
||||
func (r *TelegramChannelRepositoryImpl) FindActiveChannelsByTypes(chatTypes []string) ([]entity.TelegramChannel, error) {
|
||||
var channels []entity.TelegramChannel
|
||||
err := r.db.Where("chat_type IN (?) AND is_active = ?", chatTypes, true).Find(&channels).Error
|
||||
return channels, err
|
||||
}
|
||||
|
||||
// UpdateLastPushAt 更新最后推送时间
|
||||
func (r *TelegramChannelRepositoryImpl) UpdateLastPushAt(id uint, lastPushAt time.Time) error {
|
||||
return r.db.Model(&entity.TelegramChannel{}).Where("id = ?", id).Update("last_push_at", lastPushAt).Error
|
||||
|
||||
@@ -20,7 +20,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
image: ctwj/urldb-backend:1.2.5
|
||||
image: ctwj/urldb-backend:1.3.3
|
||||
environment:
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
image: ctwj/urldb-frontend:1.2.5
|
||||
image: ctwj/urldb-frontend:1.3.3
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NUXT_PUBLIC_API_SERVER: http://backend:8080/api
|
||||
|
||||
132
docs/logging.md
Normal file
132
docs/logging.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# 日志系统说明
|
||||
|
||||
## 概述
|
||||
|
||||
本项目使用自定义的日志系统,支持多种日志级别、环境差异化配置和结构化日志记录。
|
||||
|
||||
## 日志级别
|
||||
|
||||
日志系统支持以下级别(按严重程度递增):
|
||||
|
||||
1. **DEBUG** - 调试信息,用于开发和故障排除
|
||||
2. **INFO** - 一般信息,记录系统正常运行状态
|
||||
3. **WARN** - 警告信息,表示可能的问题但不影响系统运行
|
||||
4. **ERROR** - 错误信息,表示系统错误但可以继续运行
|
||||
5. **FATAL** - 致命错误,系统将退出
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 日志级别配置
|
||||
|
||||
可以通过环境变量配置日志级别:
|
||||
|
||||
```bash
|
||||
# 设置日志级别(DEBUG, INFO, WARN, ERROR, FATAL)
|
||||
LOG_LEVEL=DEBUG
|
||||
|
||||
# 或者启用调试模式(等同于DEBUG级别)
|
||||
DEBUG=true
|
||||
```
|
||||
|
||||
默认情况下,开发环境使用DEBUG级别,生产环境使用INFO级别。
|
||||
|
||||
### 结构化日志
|
||||
|
||||
可以通过环境变量启用结构化日志(JSON格式):
|
||||
|
||||
```bash
|
||||
# 启用结构化日志
|
||||
STRUCTURED_LOG=true
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本日志记录
|
||||
|
||||
```go
|
||||
import "github.com/ctwj/urldb/utils"
|
||||
|
||||
// 基本日志记录
|
||||
utils.Debug("调试信息: %s", debugInfo)
|
||||
utils.Info("一般信息: %s", info)
|
||||
utils.Warn("警告信息: %s", warning)
|
||||
utils.Error("错误信息: %s", err)
|
||||
utils.Fatal("致命错误: %s", fatalErr) // 程序将退出
|
||||
```
|
||||
|
||||
### 结构化日志记录
|
||||
|
||||
结构化日志允许添加额外的字段信息,便于日志分析:
|
||||
|
||||
```go
|
||||
// 带字段的结构化日志
|
||||
utils.DebugWithFields(map[string]interface{}{
|
||||
"user_id": 123,
|
||||
"action": "login",
|
||||
"ip": "192.168.1.1",
|
||||
}, "用户登录调试信息")
|
||||
|
||||
utils.InfoWithFields(map[string]interface{}{
|
||||
"task_id": 456,
|
||||
"status": "completed",
|
||||
"duration_ms": 1250,
|
||||
}, "任务处理完成")
|
||||
|
||||
utils.ErrorWithFields(map[string]interface{}{
|
||||
"error_code": 500,
|
||||
"error": "database connection failed",
|
||||
"component": "database",
|
||||
}, "数据库连接失败: %v", err)
|
||||
```
|
||||
|
||||
## 日志输出
|
||||
|
||||
日志默认输出到:
|
||||
- 控制台(标准输出)
|
||||
- 文件(logs目录下的app_日期.log文件)
|
||||
|
||||
日志文件支持轮转,单个文件最大100MB,最多保留5个备份文件,日志文件最长保留30天。
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **选择合适的日志级别**:
|
||||
- DEBUG:详细的调试信息,仅在开发和故障排除时使用
|
||||
- INFO:重要的业务流程和状态变更
|
||||
- WARN:可预期的问题和异常情况
|
||||
- ERROR:系统错误和异常
|
||||
- FATAL:系统无法继续运行的致命错误
|
||||
|
||||
2. **使用结构化日志**:
|
||||
- 对于需要后续分析的日志,使用结构化日志
|
||||
- 添加有意义的字段,如用户ID、任务ID、请求ID等
|
||||
- 避免在字段中包含敏感信息
|
||||
|
||||
3. **性能监控**:
|
||||
- 记录关键操作的执行时间
|
||||
- 使用duration_ms字段记录毫秒级耗时
|
||||
|
||||
4. **安全日志**:
|
||||
- 记录所有认证和授权相关的操作
|
||||
- 包含客户端IP和用户信息
|
||||
- 记录失败的访问尝试
|
||||
|
||||
## 示例
|
||||
|
||||
```go
|
||||
// 性能监控示例
|
||||
startTime := time.Now()
|
||||
// 执行操作...
|
||||
duration := time.Since(startTime)
|
||||
utils.DebugWithFields(map[string]interface{}{
|
||||
"operation": "database_query",
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
}, "数据库查询完成,耗时: %v", duration)
|
||||
|
||||
// 安全日志示例
|
||||
utils.InfoWithFields(map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"ip": clientIP,
|
||||
"action": "login",
|
||||
"status": "success",
|
||||
}, "用户登录成功 - 用户ID: %d, IP: %s", userID, clientIP)
|
||||
```
|
||||
2033
docs/plugin.md
Normal file
2033
docs/plugin.md
Normal file
File diff suppressed because it is too large
Load Diff
201
docs/plugin_dependency_management.md
Normal file
201
docs/plugin_dependency_management.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 插件依赖管理功能说明
|
||||
|
||||
本文档详细说明了urlDB插件系统的依赖管理功能,包括依赖解析和验证机制以及依赖加载顺序管理。
|
||||
|
||||
## 1. 设计概述
|
||||
|
||||
插件依赖管理功能允许插件声明其依赖关系,系统会在插件初始化和启动时自动验证这些依赖关系,并确保插件按照正确的顺序加载。
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **DependencyManager**: 负责依赖解析和验证的核心组件
|
||||
2. **DependencyGraph**: 依赖关系图,用于表示插件间的依赖关系
|
||||
3. **PluginLoader**: 增强的插件加载器,支持依赖管理
|
||||
4. **插件接口扩展**: 为插件添加依赖声明和检查方法
|
||||
|
||||
## 2. 依赖解析和验证机制
|
||||
|
||||
### 2.1 依赖声明
|
||||
|
||||
插件通过实现`Dependencies() []string`方法声明其依赖关系:
|
||||
|
||||
```go
|
||||
// Dependencies returns the plugin dependencies
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return []string{"database-plugin", "auth-plugin"}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 依赖检查
|
||||
|
||||
插件可以通过实现`CheckDependencies() map[string]bool`方法来检查依赖状态:
|
||||
|
||||
```go
|
||||
// CheckDependencies checks the plugin dependencies
|
||||
func (p *MyPlugin) CheckDependencies() map[string]bool {
|
||||
dependencies := p.Dependencies()
|
||||
result := make(map[string]bool)
|
||||
|
||||
for _, dep := range dependencies {
|
||||
// 检查依赖是否满足
|
||||
result[dep] = isDependencySatisfied(dep)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 系统级依赖验证
|
||||
|
||||
DependencyManager提供了以下验证功能:
|
||||
|
||||
1. **依赖存在性验证**: 确保所有声明的依赖都已注册
|
||||
2. **循环依赖检测**: 检测并防止循环依赖
|
||||
3. **依赖状态检查**: 验证依赖插件是否正在运行
|
||||
|
||||
```go
|
||||
// ValidateDependencies validates all plugin dependencies
|
||||
func (dm *DependencyManager) ValidateDependencies() error {
|
||||
// 检查所有依赖是否存在
|
||||
// 检测循环依赖
|
||||
// 返回验证结果
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 依赖加载顺序管理
|
||||
|
||||
### 3.1 拓扑排序
|
||||
|
||||
系统使用拓扑排序算法确定插件的加载顺序,确保依赖项在依赖它们的插件之前加载。
|
||||
|
||||
```go
|
||||
// GetLoadOrder returns the correct order to load plugins based on dependencies
|
||||
func (dm *DependencyManager) GetLoadOrder() ([]string, error) {
|
||||
// 构建依赖图
|
||||
// 执行拓扑排序
|
||||
// 返回加载顺序
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 加载流程
|
||||
|
||||
1. 构建依赖图
|
||||
2. 验证依赖关系
|
||||
3. 执行拓扑排序确定加载顺序
|
||||
4. 按顺序加载插件
|
||||
|
||||
## 4. 使用示例
|
||||
|
||||
### 4.1 声明依赖
|
||||
|
||||
```go
|
||||
type MyPlugin struct {
|
||||
name string
|
||||
version string
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func NewMyPlugin() *MyPlugin {
|
||||
return &MyPlugin{
|
||||
name: "my-plugin",
|
||||
version: "1.0.0",
|
||||
dependencies: []string{"demo-plugin"}, // 声明依赖
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return p.dependencies
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 检查依赖
|
||||
|
||||
```go
|
||||
func (p *MyPlugin) Start() error {
|
||||
// 检查依赖状态
|
||||
satisfied, unresolved, err := pluginManager.CheckPluginDependencies(p.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !satisfied {
|
||||
return fmt.Errorf("unsatisfied dependencies: %v", unresolved)
|
||||
}
|
||||
|
||||
// 依赖满足,继续启动
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 系统级依赖管理
|
||||
|
||||
```go
|
||||
// 验证所有依赖
|
||||
if err := pluginManager.ValidateDependencies(); err != nil {
|
||||
log.Fatalf("Dependency validation failed: %v", err)
|
||||
}
|
||||
|
||||
// 获取加载顺序
|
||||
loadOrder, err := pluginManager.GetLoadOrder()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to determine load order: %v", err)
|
||||
}
|
||||
|
||||
// 按顺序加载插件
|
||||
for _, pluginName := range loadOrder {
|
||||
if err := pluginManager.InitializePlugin(pluginName, config); err != nil {
|
||||
log.Fatalf("Failed to initialize plugin %s: %v", pluginName, err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. API参考
|
||||
|
||||
### 5.1 DependencyManager方法
|
||||
|
||||
- `ValidateDependencies() error`: 验证所有插件依赖
|
||||
- `CheckPluginDependencies(pluginName string) (bool, []string, error)`: 检查特定插件的依赖状态
|
||||
- `GetLoadOrder() ([]string, error)`: 获取插件加载顺序
|
||||
- `GetDependencyInfo(pluginName string) (*types.PluginInfo, error)`: 获取插件依赖信息
|
||||
- `CheckAllDependencies() map[string]map[string]bool`: 检查所有插件的依赖状态
|
||||
|
||||
### 5.2 PluginLoader方法
|
||||
|
||||
- `LoadPluginWithDependencies(pluginName string) error`: 加载插件及其依赖
|
||||
- `LoadAllPlugins() error`: 按依赖顺序加载所有插件
|
||||
|
||||
## 6. 最佳实践
|
||||
|
||||
1. **明确声明依赖**: 插件应明确声明所有必需的依赖
|
||||
2. **避免循环依赖**: 设计时应避免插件间的循环依赖
|
||||
3. **提供依赖检查**: 实现`CheckDependencies`方法以提供详细的依赖状态
|
||||
4. **处理依赖失败**: 优雅地处理依赖不满足的情况
|
||||
5. **测试依赖关系**: 编写测试确保依赖关系正确配置
|
||||
|
||||
## 7. 故障排除
|
||||
|
||||
### 7.1 常见错误
|
||||
|
||||
1. **依赖未找到**: 确保依赖插件已正确注册
|
||||
2. **循环依赖**: 检查插件依赖关系图,消除循环依赖
|
||||
3. **依赖未启动**: 确保依赖插件已正确启动并运行
|
||||
|
||||
### 7.2 调试工具
|
||||
|
||||
使用以下方法调试依赖问题:
|
||||
|
||||
```go
|
||||
// 检查所有依赖状态
|
||||
allDeps := pluginManager.CheckAllDependencies()
|
||||
for plugin, deps := range allDeps {
|
||||
fmt.Printf("Plugin %s dependencies: %v\n", plugin, deps)
|
||||
}
|
||||
|
||||
// 获取特定插件的依赖信息
|
||||
info, err := pluginManager.GetDependencyInfo("my-plugin")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get dependency info: %v", err)
|
||||
} else {
|
||||
fmt.Printf("Plugin info: %+v\n", info)
|
||||
}
|
||||
```
|
||||
305
docs/plugin_design.md
Normal file
305
docs/plugin_design.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# urlDB插件系统设计方案
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
本方案旨在为urlDB系统设计一个轻量级、高性能、易维护的插件系统,实现系统的模块化扩展能力。插件系统将支持动态配置、数据管理、日志记录、任务调度等功能,使新功能可以通过插件形式轻松集成到系统中。
|
||||
|
||||
### 1.2 设计原则
|
||||
- **轻量级实现**:采用进程内加载模式,避免复杂的.so文件管理
|
||||
- **与现有架构融合**:复用现有组件,保持系统一致性
|
||||
- **高性能**:最小化插件调用开销,优化内存和并发性能
|
||||
- **易维护**:提供完善的生命周期管理、监控告警和故障恢复机制
|
||||
- **安全性**:实现权限控制、数据隔离和审计日志
|
||||
|
||||
## 2. 系统架构
|
||||
|
||||
### 2.1 整体架构
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 主应用程序 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 插件管理器 │ 配置管理器 │ 数据管理器 │ 日志管理器 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 插件1 (内置) 插件2 (内置) 插件3 (内置) ... │
|
||||
│ [网盘服务] [通知服务] [统计分析] │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 核心组件
|
||||
1. **插件管理器**:负责插件的注册、发现、加载、卸载和生命周期管理
|
||||
2. **配置管理器**:管理插件配置项的定义、存储和验证
|
||||
3. **数据管理器**:管理插件数据的存储、查询和清理
|
||||
4. **日志管理器**:管理插件日志的记录、查询和清理
|
||||
|
||||
## 3. 插件接口设计
|
||||
|
||||
### 3.1 基础接口
|
||||
```go
|
||||
// 插件基础接口
|
||||
type Plugin interface {
|
||||
// 基本信息
|
||||
Name() string
|
||||
Version() string
|
||||
Description() string
|
||||
Author() string
|
||||
|
||||
// 生命周期
|
||||
Initialize(ctx PluginContext) error
|
||||
Start() error
|
||||
Stop() error
|
||||
Cleanup() error
|
||||
|
||||
// 依赖管理
|
||||
Dependencies() []string
|
||||
CheckDependencies() map[string]bool
|
||||
|
||||
// 配置管理
|
||||
ConfigSchema() *PluginConfigSchema
|
||||
ValidateConfig(config map[string]interface{}) error
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 上下文接口
|
||||
```go
|
||||
// 插件上下文接口
|
||||
type PluginContext interface {
|
||||
// 日志功能
|
||||
LogDebug(msg string, args ...interface{})
|
||||
LogInfo(msg string, args ...interface{})
|
||||
LogWarn(msg string, args ...interface{})
|
||||
LogError(msg string, args ...interface{})
|
||||
|
||||
// 配置功能
|
||||
GetConfig(key string) (interface{}, error)
|
||||
SetConfig(key string, value interface{}) error
|
||||
|
||||
// 数据功能
|
||||
GetData(key string, dataType string) (interface{}, error)
|
||||
SetData(key string, value interface{}, dataType string) error
|
||||
DeleteData(key string, dataType string) error
|
||||
|
||||
// 任务功能
|
||||
RegisterTask(name string, task func()) error
|
||||
UnregisterTask(name string) error
|
||||
|
||||
// 数据库功能
|
||||
GetDB() *gorm.DB
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 实现方式
|
||||
|
||||
### 4.1 自研轻量级实现
|
||||
基于对urlDB系统的分析,采用自研轻量级插件系统实现方式:
|
||||
|
||||
**优势**:
|
||||
- 避免Go plugin包的平台和版本限制
|
||||
- 与现有系统架构高度融合
|
||||
- 运维友好,单一二进制文件部署
|
||||
- 跨平台兼容性好
|
||||
- 性能优化,进程内调用无IPC开销
|
||||
|
||||
### 4.2 进程内加载机制
|
||||
```go
|
||||
// 插件注册阶段(编译时)
|
||||
func init() {
|
||||
pluginManager.Register(&NetworkDiskPlugin{})
|
||||
pluginManager.Register(&NotificationPlugin{})
|
||||
}
|
||||
|
||||
// 插件发现阶段(启动时)
|
||||
func (pm *PluginManager) DiscoverPlugins() {
|
||||
for _, plugin := range pm.registeredPlugins {
|
||||
if pm.isPluginEnabled(plugin.Name()) {
|
||||
pm.loadedPlugins[plugin.Name()] = plugin
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 插件生命周期管理
|
||||
|
||||
### 5.1 状态定义
|
||||
```go
|
||||
type PluginStatus string
|
||||
|
||||
const (
|
||||
StatusRegistered PluginStatus = "registered" // 已注册
|
||||
StatusInitialized PluginStatus = "initialized" // 已初始化
|
||||
StatusStarting PluginStatus = "starting" // 启动中
|
||||
StatusRunning PluginStatus = "running" // 运行中
|
||||
StatusStopping PluginStatus = "stopping" // 停止中
|
||||
StatusStopped PluginStatus = "stopped" // 已停止
|
||||
StatusError PluginStatus = "error" // 错误状态
|
||||
StatusDisabled PluginStatus = "disabled" // 已禁用
|
||||
)
|
||||
```
|
||||
|
||||
### 5.2 启动流程
|
||||
1. 状态检查
|
||||
2. 依赖验证
|
||||
3. 插件初始化
|
||||
4. 启动插件服务
|
||||
5. 健康检查启动
|
||||
6. 状态更新
|
||||
|
||||
### 5.3 停止流程
|
||||
1. 状态更新为停止中
|
||||
2. 停止健康检查
|
||||
3. 优雅停止或强制停止
|
||||
4. 状态更新
|
||||
5. 资源清理
|
||||
|
||||
## 6. 与现有系统集成
|
||||
|
||||
### 6.1 统一入口模式
|
||||
通过插件管理器作为中央协调器,与现有系统组件集成:
|
||||
- 复用Repository管理器
|
||||
- 复用配置管理器
|
||||
- 复用任务管理器
|
||||
- 复用日志系统
|
||||
- 复用监控系统
|
||||
|
||||
### 6.2 HTTP路由集成
|
||||
```go
|
||||
// 插件路由自动注册
|
||||
func setupPluginRoutes(router *gin.Engine, pm *plugin.Manager) {
|
||||
plugins := pm.GetEnabledPlugins()
|
||||
for _, plugin := range plugins {
|
||||
if httpHandler, ok := plugin.(HTTPHandler); ok {
|
||||
pluginGroup := router.Group(fmt.Sprintf("/api/plugins/%s", plugin.Name()))
|
||||
httpHandler.RegisterRoutes(pluginGroup)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 任务调度集成
|
||||
```go
|
||||
// 插件任务处理器注册
|
||||
func registerPluginTasks(pm *plugin.Manager, taskManager *task.TaskManager) {
|
||||
plugins := pm.GetEnabledPlugins()
|
||||
for _, plugin := range plugins {
|
||||
if taskHandler, ok := plugin.(TaskHandler); ok {
|
||||
taskManager.RegisterProcessor(&PluginTaskProcessor{
|
||||
plugin: plugin,
|
||||
handler: taskHandler,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 性能优化策略
|
||||
|
||||
### 7.1 内存优化
|
||||
- 插件实例池化复用
|
||||
- 内存泄漏防护监控
|
||||
- 分段锁减少锁竞争
|
||||
|
||||
### 7.2 并发优化
|
||||
- 协程池管理任务执行
|
||||
- 读写锁优化并发访问
|
||||
- 批量操作减少数据库交互
|
||||
|
||||
### 7.3 缓存优化
|
||||
- 多级缓存架构(L1内存、L2共享缓存)
|
||||
- 智能缓存失效策略
|
||||
- 缓存预热机制
|
||||
|
||||
### 7.4 数据库优化
|
||||
- 批量操作优化
|
||||
- 查询优化和慢查询监控
|
||||
- 连接池管理
|
||||
|
||||
## 8. 安全性设计
|
||||
|
||||
### 8.1 权限控制
|
||||
- 插件加载权限限制
|
||||
- 插件配置访问控制
|
||||
- 数据访问权限隔离
|
||||
- 日志查看权限管理
|
||||
|
||||
### 8.2 数据安全
|
||||
- 敏感配置项加密存储
|
||||
- 插件数据隔离
|
||||
- 审计日志记录
|
||||
|
||||
### 8.3 运行安全
|
||||
- 插件沙箱隔离
|
||||
- 资源使用限制
|
||||
- 异常行为监控
|
||||
|
||||
## 9. 运维管理
|
||||
|
||||
### 9.1 部署架构
|
||||
- 容器化部署支持
|
||||
- Kubernetes部署配置
|
||||
- 配置文件管理
|
||||
|
||||
### 9.2 监控告警
|
||||
- 健康检查端点
|
||||
- 性能监控指标
|
||||
- Prometheus集成
|
||||
|
||||
### 9.3 日志管理
|
||||
- 结构化日志输出
|
||||
- 日志轮转配置
|
||||
- 日志分析工具
|
||||
|
||||
### 9.4 故障排查
|
||||
- 常见问题诊断命令
|
||||
- 性能分析工具集成
|
||||
- 故障恢复流程
|
||||
|
||||
### 9.5 备份恢复
|
||||
- 配置备份策略
|
||||
- 数据备份机制
|
||||
- 灾难恢复流程
|
||||
|
||||
### 9.6 升级维护
|
||||
- 灰度发布策略
|
||||
- 版本兼容性管理
|
||||
- 热升级支持
|
||||
|
||||
## 10. 实施计划
|
||||
|
||||
### 10.1 第一阶段:基础框架
|
||||
- 实现插件管理器核心功能
|
||||
- 完成插件生命周期管理
|
||||
- 集成现有系统组件
|
||||
|
||||
### 10.2 第二阶段:配置管理
|
||||
- 实现插件配置管理
|
||||
- 开发配置UI界面
|
||||
- 完成配置验证机制
|
||||
|
||||
### 10.3 第三阶段:数据日志
|
||||
- 实现插件数据管理
|
||||
- 完成日志管理功能
|
||||
- 开发数据查看界面
|
||||
|
||||
### 10.4 第四阶段:安全运维
|
||||
- 实现安全控制机制
|
||||
- 完善监控告警系统
|
||||
- 编写运维文档
|
||||
|
||||
## 11. 风险评估与应对
|
||||
|
||||
### 11.1 技术风险
|
||||
- **性能影响**:通过性能测试和优化确保系统性能
|
||||
- **稳定性问题**:实现完善的异常处理和故障恢复机制
|
||||
- **兼容性问题**:制定版本兼容性管理策略
|
||||
|
||||
### 11.2 运维风险
|
||||
- **部署复杂性**:提供详细的部署文档和自动化脚本
|
||||
- **故障排查困难**:完善监控告警和日志分析工具
|
||||
- **数据安全风险**:实现数据加密和访问控制
|
||||
|
||||
### 11.3 管理风险
|
||||
- **插件质量控制**:建立插件开发规范和测试机制
|
||||
- **版本管理混乱**:制定版本管理策略和升级流程
|
||||
- **权限管理不当**:实现细粒度的权限控制机制
|
||||
379
docs/plugin_development.md
Normal file
379
docs/plugin_development.md
Normal file
@@ -0,0 +1,379 @@
|
||||
# 插件系统开发指南
|
||||
|
||||
## 概述
|
||||
|
||||
urlDB插件系统提供了一个灵活的扩展机制,允许开发者创建自定义功能来增强系统能力。插件采用进程内加载模式,避免使用Go标准plugin包的限制。
|
||||
|
||||
## 插件生命周期
|
||||
|
||||
1. **注册 (Register)** - 插件被发现并注册到管理器
|
||||
2. **初始化 (Initialize)** - 插件接收上下文并准备运行
|
||||
3. **启动 (Start)** - 插件开始执行主要功能
|
||||
4. **运行 (Running)** - 插件正常工作状态
|
||||
5. **停止 (Stop)** - 插件停止运行
|
||||
6. **清理 (Cleanup)** - 插件释放资源
|
||||
|
||||
## 创建插件
|
||||
|
||||
### 基本插件结构
|
||||
|
||||
### 可配置插件
|
||||
|
||||
插件可以实现 `ConfigurablePlugin` 接口来支持配置模式和模板:
|
||||
|
||||
```go
|
||||
// ConfigurablePlugin is an optional interface for plugins that support configuration schemas
|
||||
type ConfigurablePlugin interface {
|
||||
// CreateConfigSchema creates the configuration schema for the plugin
|
||||
CreateConfigSchema() *config.ConfigSchema
|
||||
|
||||
// CreateConfigTemplate creates a default configuration template
|
||||
CreateConfigTemplate() *config.ConfigTemplate
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
package myplugin
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin/config"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// MyPlugin 实现插件接口
|
||||
type MyPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
// NewMyPlugin 创建新插件实例
|
||||
func NewMyPlugin() *MyPlugin {
|
||||
return &MyPlugin{
|
||||
name: "my-plugin",
|
||||
version: "1.0.0",
|
||||
description: "我的自定义插件",
|
||||
author: "开发者名称",
|
||||
}
|
||||
}
|
||||
|
||||
// 实现必需的接口方法
|
||||
func (p *MyPlugin) Name() string { return p.name }
|
||||
func (p *MyPlugin) Version() string { return p.version }
|
||||
func (p *MyPlugin) Description() string { return p.description }
|
||||
func (p *MyPlugin) Author() string { return p.author }
|
||||
|
||||
func (p *MyPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
p.context.LogInfo("插件初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Start() error {
|
||||
p.context.LogInfo("插件启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Stop() error {
|
||||
p.context.LogInfo("插件停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Cleanup() error {
|
||||
p.context.LogInfo("插件清理")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (p *MyPlugin) CheckDependencies() map[string]bool {
|
||||
return make(map[string]bool)
|
||||
}
|
||||
|
||||
// 实现可选的配置接口
|
||||
func (p *MyPlugin) CreateConfigSchema() *config.ConfigSchema {
|
||||
schema := config.NewConfigSchema(p.name, p.version)
|
||||
|
||||
// 添加配置字段
|
||||
intervalMin := 1.0
|
||||
intervalMax := 3600.0
|
||||
schema.AddField(config.ConfigField{
|
||||
Key: "interval",
|
||||
Name: "检查间隔",
|
||||
Description: "插件执行任务的时间间隔(秒)",
|
||||
Type: "int",
|
||||
Required: true,
|
||||
Default: 60,
|
||||
Min: &intervalMin,
|
||||
Max: &intervalMax,
|
||||
})
|
||||
|
||||
schema.AddField(config.ConfigField{
|
||||
Key: "enabled",
|
||||
Name: "启用状态",
|
||||
Description: "插件是否启用",
|
||||
Type: "bool",
|
||||
Required: true,
|
||||
Default: true,
|
||||
})
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
func (p *MyPlugin) CreateConfigTemplate() *config.ConfigTemplate {
|
||||
config := map[string]interface{}{
|
||||
"interval": 30,
|
||||
"enabled": true,
|
||||
}
|
||||
|
||||
return &config.ConfigTemplate{
|
||||
Name: "default-config",
|
||||
Description: "默认配置模板",
|
||||
Config: config,
|
||||
Version: p.version,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 插件上下文 API
|
||||
|
||||
### 日志功能
|
||||
|
||||
```go
|
||||
// 记录不同级别的日志
|
||||
p.context.LogDebug("调试信息: %s", "详细信息")
|
||||
p.context.LogInfo("普通信息: %d", 42)
|
||||
p.context.LogWarn("警告信息")
|
||||
p.context.LogError("错误信息: %v", err)
|
||||
```
|
||||
|
||||
### 配置管理
|
||||
|
||||
```go
|
||||
// 设置配置
|
||||
err := p.context.SetConfig("interval", 60)
|
||||
err := p.context.SetConfig("enabled", true)
|
||||
err := p.context.SetConfig("api_key", "secret-key")
|
||||
|
||||
// 获取配置
|
||||
interval, err := p.context.GetConfig("interval")
|
||||
enabled, err := p.context.GetConfig("enabled")
|
||||
```
|
||||
|
||||
### 数据存储
|
||||
|
||||
```go
|
||||
// 存储数据
|
||||
data := map[string]interface{}{
|
||||
"last_update": time.Now().Unix(),
|
||||
"counter": 0,
|
||||
"status": "active",
|
||||
}
|
||||
err := p.context.SetData("my_data", data, "app_state")
|
||||
|
||||
// 读取数据
|
||||
retrievedData, err := p.context.GetData("my_data", "app_state")
|
||||
|
||||
// 删除数据
|
||||
err := p.context.DeleteData("my_data", "app_state")
|
||||
```
|
||||
|
||||
### 数据库访问
|
||||
|
||||
```go
|
||||
// 获取数据库连接
|
||||
db := p.context.GetDB()
|
||||
if gormDB, ok := db.(*gorm.DB); ok {
|
||||
// 执行数据库操作
|
||||
var count int64
|
||||
err := gormDB.Model(&entity.Resource{}).Count(&count).Error
|
||||
if err != nil {
|
||||
p.context.LogError("查询失败: %v", err)
|
||||
} else {
|
||||
p.context.LogInfo("资源数量: %d", count)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 任务调度
|
||||
|
||||
```go
|
||||
// 注册定时任务
|
||||
err := p.context.RegisterTask("my-periodic-task", func() {
|
||||
p.context.LogInfo("执行定时任务于 %s", time.Now().Format(time.RFC3339))
|
||||
// 任务逻辑...
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
p.context.LogError("注册任务失败: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## 自动注册插件
|
||||
|
||||
```go
|
||||
package myplugin
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// init 函数在包导入时自动调用
|
||||
func init() {
|
||||
plugin := NewMyPlugin()
|
||||
RegisterPlugin(plugin)
|
||||
}
|
||||
|
||||
// RegisterPlugin 注册插件到全局管理器
|
||||
func RegisterPlugin(pluginInstance types.Plugin) {
|
||||
if plugin.GetManager() != nil {
|
||||
plugin.GetManager().RegisterPlugin(pluginInstance)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例插件
|
||||
|
||||
```go
|
||||
package demo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FullDemoPlugin 完整功能演示插件
|
||||
type FullDemoPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
func NewFullDemoPlugin() *FullDemoPlugin {
|
||||
return &FullDemoPlugin{
|
||||
name: "full-demo-plugin",
|
||||
version: "1.0.0",
|
||||
description: "完整功能演示插件",
|
||||
author: "urlDB Team",
|
||||
}
|
||||
}
|
||||
|
||||
// ... 实现接口方法
|
||||
|
||||
func (p *FullDemoPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
p.context.LogInfo("演示插件初始化")
|
||||
|
||||
// 设置配置
|
||||
p.context.SetConfig("interval", 60)
|
||||
p.context.SetConfig("enabled", true)
|
||||
|
||||
// 存储初始数据
|
||||
data := map[string]interface{}{
|
||||
"start_time": time.Now().Format(time.RFC3339),
|
||||
"counter": 0,
|
||||
}
|
||||
p.context.SetData("demo_stats", data, "monitoring")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *FullDemoPlugin) Start() error {
|
||||
p.context.LogInfo("演示插件启动")
|
||||
|
||||
// 注册定时任务
|
||||
err := p.context.RegisterTask("demo-task", p.demoTask)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 演示数据库访问
|
||||
p.demoDatabaseAccess()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *FullDemoPlugin) demoTask() {
|
||||
p.context.LogInfo("执行演示任务")
|
||||
|
||||
// 更新计数器
|
||||
data, err := p.context.GetData("demo_stats", "monitoring")
|
||||
if err == nil {
|
||||
if stats, ok := data.(map[string]interface{}); ok {
|
||||
counter, _ := stats["counter"].(float64)
|
||||
stats["counter"] = counter + 1
|
||||
stats["last_update"] = time.Now().Format(time.RFC3339)
|
||||
p.context.SetData("demo_stats", stats, "monitoring")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **错误处理**:始终检查错误并适当处理
|
||||
2. **资源清理**:在Cleanup方法中释放所有资源
|
||||
3. **配置验证**:在初始化时验证配置有效性
|
||||
4. **日志记录**:使用适当的日志级别记录重要事件
|
||||
5. **性能考虑**:避免在插件中执行阻塞操作
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 插件如何访问数据库?
|
||||
A: 通过 `p.context.GetDB()` 获取数据库连接,转换为 `*gorm.DB` 后使用。
|
||||
|
||||
### Q: 插件如何存储持久化数据?
|
||||
A: 使用 `SetData()`、`GetData()`、`DeleteData()` 方法。
|
||||
|
||||
### Q: 插件如何注册定时任务?
|
||||
A: 使用 `RegisterTask()` 方法注册任务函数。
|
||||
|
||||
### Q: 插件如何记录日志?
|
||||
A: 使用 `LogDebug()`、`LogInfo()`、`LogWarn()`、`LogError()` 方法。
|
||||
|
||||
## 部署流程
|
||||
|
||||
1. 将插件代码放在 `plugin/` 目录下
|
||||
2. 确保包含自动注册的 `init()` 函数
|
||||
3. 构建主应用程序:`go build -o main .`
|
||||
4. 启动应用程序,插件将自动注册
|
||||
5. 通过API或管理界面启用插件
|
||||
|
||||
## API参考
|
||||
|
||||
### PluginContext 接口
|
||||
|
||||
- `LogDebug(msg string, args ...interface{})` - 调试日志
|
||||
- `LogInfo(msg string, args ...interface{})` - 信息日志
|
||||
- `LogWarn(msg string, args ...interface{})` - 警告日志
|
||||
- `LogError(msg string, args ...interface{})` - 错误日志
|
||||
- `GetConfig(key string) (interface{}, error)` - 获取配置
|
||||
- `SetConfig(key string, value interface{}) error` - 设置配置
|
||||
- `GetData(key string, dataType string) (interface{}, error)` - 获取数据
|
||||
- `SetData(key string, value interface{}, dataType string) error` - 设置数据
|
||||
- `DeleteData(key string, dataType string) error` - 删除数据
|
||||
- `RegisterTask(name string, taskFunc func()) error` - 注册任务
|
||||
- `GetDB() interface{}` - 获取数据库连接
|
||||
|
||||
### Plugin 接口
|
||||
|
||||
- `Name() string` - 插件名称
|
||||
- `Version() string` - 插件版本
|
||||
- `Description() string` - 插件描述
|
||||
- `Author() string` - 插件作者
|
||||
- `Initialize(ctx PluginContext) error` - 初始化插件
|
||||
- `Start() error` - 启动插件
|
||||
- `Stop() error` - 停止插件
|
||||
- `Cleanup() error` - 清理插件
|
||||
- `Dependencies() []string` - 获取依赖
|
||||
- `CheckDependencies() map[string]bool` - 检查依赖
|
||||
736
docs/plugin_guide.md
Normal file
736
docs/plugin_guide.md
Normal file
@@ -0,0 +1,736 @@
|
||||
# urlDB插件开发指南
|
||||
|
||||
## 目录
|
||||
1. [插件系统概述](#插件系统概述)
|
||||
2. [插件开发规范](#插件开发规范)
|
||||
3. [插件实现步骤](#插件实现步骤)
|
||||
4. [插件编译](#插件编译)
|
||||
5. [插件加载和使用](#插件加载和使用)
|
||||
6. [插件配置管理](#插件配置管理)
|
||||
7. [插件数据管理](#插件数据管理)
|
||||
8. [插件日志记录](#插件日志记录)
|
||||
9. [插件任务调度](#插件任务调度)
|
||||
10. [插件生命周期管理](#插件生命周期管理)
|
||||
11. [插件依赖管理](#插件依赖管理)
|
||||
12. [插件安全和权限](#插件安全和权限)
|
||||
13. [插件测试](#插件测试)
|
||||
14. [最佳实践](#最佳实践)
|
||||
|
||||
## 插件系统概述
|
||||
|
||||
urlDB插件系统是一个轻量级、高性能的插件框架,旨在为系统提供模块化扩展能力。插件系统采用进程内加载模式,与现有系统架构高度融合,提供完整的生命周期管理、配置管理、数据管理、日志记录和任务调度功能。
|
||||
|
||||
### 核心特性
|
||||
- **轻量级实现**:避免复杂的.so文件管理
|
||||
- **高性能**:进程内调用,无IPC开销
|
||||
- **易集成**:与现有系统组件无缝集成
|
||||
- **完整功能**:支持配置、数据、日志、任务等核心功能
|
||||
- **安全可靠**:提供权限控制和资源隔离
|
||||
|
||||
## 插件开发规范
|
||||
|
||||
### 基础接口规范
|
||||
所有插件必须实现`types.Plugin`接口:
|
||||
|
||||
```go
|
||||
type Plugin interface {
|
||||
// 基本信息
|
||||
Name() string
|
||||
Version() string
|
||||
Description() string
|
||||
Author() string
|
||||
|
||||
// 生命周期
|
||||
Initialize(ctx PluginContext) error
|
||||
Start() error
|
||||
Stop() error
|
||||
Cleanup() error
|
||||
|
||||
// 依赖管理
|
||||
Dependencies() []string
|
||||
CheckDependencies() map[string]bool
|
||||
}
|
||||
```
|
||||
|
||||
### 上下文接口规范
|
||||
插件通过`PluginContext`与系统交互:
|
||||
|
||||
```go
|
||||
type PluginContext interface {
|
||||
// 日志功能
|
||||
LogDebug(msg string, args ...interface{})
|
||||
LogInfo(msg string, args ...interface{})
|
||||
LogWarn(msg string, args ...interface{})
|
||||
LogError(msg string, args ...interface{})
|
||||
|
||||
// 配置功能
|
||||
GetConfig(key string) (interface{}, error)
|
||||
SetConfig(key string, value interface{}) error
|
||||
|
||||
// 数据功能
|
||||
GetData(key string, dataType string) (interface{}, error)
|
||||
SetData(key string, value interface{}, dataType string) error
|
||||
DeleteData(key string, dataType string) error
|
||||
|
||||
// 任务功能
|
||||
RegisterTask(name string, task func()) error
|
||||
UnregisterTask(name string) error
|
||||
|
||||
// 数据库功能
|
||||
GetDB() interface{} // 返回 *gorm.DB
|
||||
}
|
||||
```
|
||||
|
||||
## 插件实现步骤
|
||||
|
||||
### 1. 创建插件结构体
|
||||
```go
|
||||
package myplugin
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
type MyPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
func NewMyPlugin() *MyPlugin {
|
||||
return &MyPlugin{
|
||||
name: "my-plugin",
|
||||
version: "1.0.0",
|
||||
description: "My custom plugin",
|
||||
author: "Developer Name",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 实现基础接口方法
|
||||
```go
|
||||
// Name returns the plugin name
|
||||
func (p *MyPlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Version returns the plugin version
|
||||
func (p *MyPlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
// Description returns the plugin description
|
||||
func (p *MyPlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// Author returns the plugin author
|
||||
func (p *MyPlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
// Dependencies returns plugin dependencies
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckDependencies checks plugin dependencies
|
||||
func (p *MyPlugin) CheckDependencies() map[string]bool {
|
||||
return make(map[string]bool)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 实现生命周期方法
|
||||
```go
|
||||
// Initialize initializes the plugin
|
||||
func (p *MyPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
p.context.LogInfo("Plugin initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the plugin
|
||||
func (p *MyPlugin) Start() error {
|
||||
p.context.LogInfo("Plugin started")
|
||||
// 注册定时任务或其他初始化工作
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the plugin
|
||||
func (p *MyPlugin) Stop() error {
|
||||
p.context.LogInfo("Plugin stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the plugin
|
||||
func (p *MyPlugin) Cleanup() error {
|
||||
p.context.LogInfo("Plugin cleaned up")
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 实现插件功能
|
||||
```go
|
||||
// 示例:实现一个定时任务
|
||||
func (p *MyPlugin) doWork() {
|
||||
p.context.LogInfo("Doing work...")
|
||||
// 实际业务逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 插件编译
|
||||
|
||||
### 1. 创建插件目录
|
||||
```bash
|
||||
mkdir -p /Users/kerwin/Program/go/urldb/plugin/myplugin
|
||||
```
|
||||
|
||||
### 2. 编写插件代码
|
||||
创建`myplugin.go`文件并实现插件逻辑。
|
||||
|
||||
### 3. 编译插件
|
||||
由于采用进程内加载模式,插件需要在主程序中注册:
|
||||
|
||||
```go
|
||||
// 在main.go中注册插件
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/myplugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化插件系统
|
||||
plugin.InitPluginSystem(taskManager)
|
||||
|
||||
// 注册插件
|
||||
myPlugin := myplugin.NewMyPlugin()
|
||||
plugin.RegisterPlugin(myPlugin)
|
||||
|
||||
// 其他初始化代码...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 构建主程序
|
||||
```bash
|
||||
cd /Users/kerwin/Program/go/urldb
|
||||
go build -o urldb .
|
||||
```
|
||||
|
||||
## 插件加载和使用
|
||||
|
||||
### 1. 插件自动注册
|
||||
插件通过`init()`函数或在主程序中手动注册:
|
||||
|
||||
```go
|
||||
// 方法1: 在插件包中使用init函数自动注册
|
||||
func init() {
|
||||
plugin.RegisterPlugin(NewMyPlugin())
|
||||
}
|
||||
|
||||
// 方法2: 在main.go中手动注册
|
||||
func main() {
|
||||
plugin.InitPluginSystem(taskManager)
|
||||
plugin.RegisterPlugin(myplugin.NewMyPlugin())
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 插件配置
|
||||
在系统启动时配置插件:
|
||||
|
||||
```go
|
||||
// 初始化插件配置
|
||||
config := map[string]interface{}{
|
||||
"setting1": "value1",
|
||||
"setting2": 42,
|
||||
}
|
||||
|
||||
// 初始化插件
|
||||
if err := pluginManager.InitializePlugin("my-plugin", config); err != nil {
|
||||
utils.Error("Failed to initialize plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 启动插件
|
||||
if err := pluginManager.StartPlugin("my-plugin"); err != nil {
|
||||
utils.Error("Failed to start plugin: %v", err)
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 插件状态管理
|
||||
```go
|
||||
// 获取插件状态
|
||||
status := pluginManager.GetPluginStatus("my-plugin")
|
||||
|
||||
// 停止插件
|
||||
if err := pluginManager.StopPlugin("my-plugin"); err != nil {
|
||||
utils.Error("Failed to stop plugin: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
## 插件配置管理
|
||||
|
||||
### 1. 获取配置
|
||||
```go
|
||||
func (p *MyPlugin) doWork() {
|
||||
// 获取配置值
|
||||
setting1, err := p.context.GetConfig("setting1")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to get setting1: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Setting1 value: %v", setting1)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 设置配置
|
||||
```go
|
||||
func (p *MyPlugin) updateConfig() {
|
||||
// 设置配置值
|
||||
err := p.context.SetConfig("new_setting", "new_value")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to set config: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Config updated successfully")
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置模式定义
|
||||
```go
|
||||
// 插件可以定义自己的配置模式
|
||||
type ConfigSchema struct {
|
||||
Setting1 string `json:"setting1" validate:"required"`
|
||||
Setting2 int `json:"setting2" validate:"min=1,max=100"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
```
|
||||
|
||||
## 插件数据管理
|
||||
|
||||
### 1. 存储数据
|
||||
```go
|
||||
func (p *MyPlugin) saveData() {
|
||||
data := map[string]interface{}{
|
||||
"key1": "value1",
|
||||
"key2": 123,
|
||||
}
|
||||
|
||||
// 存储数据
|
||||
err := p.context.SetData("my_data_key", data, "my_data_type")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to save data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Data saved successfully")
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 读取数据
|
||||
```go
|
||||
func (p *MyPlugin) loadData() {
|
||||
// 读取数据
|
||||
data, err := p.context.GetData("my_data_key", "my_data_type")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to load data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Loaded data: %v", data)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 删除数据
|
||||
```go
|
||||
func (p *MyPlugin) deleteData() {
|
||||
// 删除数据
|
||||
err := p.context.DeleteData("my_data_key", "my_data_type")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to delete data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Data deleted successfully")
|
||||
}
|
||||
```
|
||||
|
||||
## 插件日志记录
|
||||
|
||||
### 1. 日志级别
|
||||
```go
|
||||
func (p *MyPlugin) logExamples() {
|
||||
// 调试日志
|
||||
p.context.LogDebug("Debug message: %s", "debug info")
|
||||
|
||||
// 信息日志
|
||||
p.context.LogInfo("Info message: %s", "process started")
|
||||
|
||||
// 警告日志
|
||||
p.context.LogWarn("Warning message: %s", "deprecated feature used")
|
||||
|
||||
// 错误日志
|
||||
p.context.LogError("Error message: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 结构化日志
|
||||
```go
|
||||
func (p *MyPlugin) structuredLogging() {
|
||||
p.context.LogInfo("Processing resource: id=%d, title=%s, url=%s",
|
||||
resource.ID, resource.Title, resource.URL)
|
||||
}
|
||||
```
|
||||
|
||||
## 插件任务调度
|
||||
|
||||
### 1. 注册定时任务
|
||||
```go
|
||||
func (p *MyPlugin) Start() error {
|
||||
p.context.LogInfo("Plugin started")
|
||||
|
||||
// 注册定时任务
|
||||
return p.context.RegisterTask("my-task", p.scheduledTask)
|
||||
}
|
||||
|
||||
func (p *MyPlugin) scheduledTask() {
|
||||
p.context.LogInfo("Scheduled task executed")
|
||||
// 执行定时任务逻辑
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 取消任务
|
||||
```go
|
||||
func (p *MyPlugin) Stop() error {
|
||||
// 取消定时任务
|
||||
err := p.context.UnregisterTask("my-task")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to unregister task: %v", err)
|
||||
}
|
||||
|
||||
p.context.LogInfo("Plugin stopped")
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 插件生命周期管理
|
||||
|
||||
### 1. 状态转换
|
||||
```
|
||||
Registered → Initialized → Starting → Running → Stopping → Stopped
|
||||
↑ ↓
|
||||
└────────────── Cleanup ←───────────────────┘
|
||||
```
|
||||
|
||||
### 2. 状态检查
|
||||
```go
|
||||
func (p *MyPlugin) checkStatus() {
|
||||
// 在插件方法中检查状态
|
||||
if p.context == nil {
|
||||
p.context.LogError("Plugin not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
// 执行业务逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 插件依赖管理
|
||||
|
||||
### 1. 声明依赖
|
||||
```go
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return []string{"database-plugin", "auth-plugin"}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查依赖
|
||||
```go
|
||||
func (p *MyPlugin) CheckDependencies() map[string]bool {
|
||||
deps := make(map[string]bool)
|
||||
deps["database-plugin"] = plugin.GetManager().GetPluginStatus("database-plugin") == types.StatusRunning
|
||||
deps["auth-plugin"] = plugin.GetManager().GetPluginStatus("auth-plugin") == types.StatusRunning
|
||||
return deps
|
||||
}
|
||||
```
|
||||
|
||||
## 插件安全和权限
|
||||
|
||||
### 1. 数据库访问控制
|
||||
```go
|
||||
func (p *MyPlugin) accessDatabase() {
|
||||
// 获取数据库连接
|
||||
db := p.context.GetDB().(*gorm.DB)
|
||||
|
||||
// 执行安全的数据库操作
|
||||
var resources []Resource
|
||||
err := db.Where("is_public = ?", true).Find(&resources).Error
|
||||
if err != nil {
|
||||
p.context.LogError("Database query failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Found %d public resources", len(resources))
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 权限检查
|
||||
```go
|
||||
func (p *MyPlugin) checkPermissions() bool {
|
||||
// 检查插件权限
|
||||
// 在实际实现中,可以从配置或上下文中获取权限信息
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
## 插件测试
|
||||
|
||||
### 1. 单元测试
|
||||
```go
|
||||
func TestMyPlugin_Name(t *testing.T) {
|
||||
plugin := NewMyPlugin()
|
||||
expected := "my-plugin"
|
||||
if plugin.Name() != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, plugin.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMyPlugin_Initialize(t *testing.T) {
|
||||
plugin := NewMyPlugin()
|
||||
mockContext := &MockPluginContext{}
|
||||
|
||||
err := plugin.Initialize(mockContext)
|
||||
if err != nil {
|
||||
t.Errorf("Initialize failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 集成测试
|
||||
```go
|
||||
func TestMyPlugin_Lifecycle(t *testing.T) {
|
||||
plugin := NewMyPlugin()
|
||||
mockContext := &MockPluginContext{}
|
||||
|
||||
// 测试完整生命周期
|
||||
if err := plugin.Initialize(mockContext); err != nil {
|
||||
t.Fatalf("Initialize failed: %v", err)
|
||||
}
|
||||
|
||||
if err := plugin.Start(); err != nil {
|
||||
t.Fatalf("Start failed: %v", err)
|
||||
}
|
||||
|
||||
if err := plugin.Stop(); err != nil {
|
||||
t.Fatalf("Stop failed: %v", err)
|
||||
}
|
||||
|
||||
if err := plugin.Cleanup(); err != nil {
|
||||
t.Fatalf("Cleanup failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 错误处理
|
||||
```go
|
||||
func (p *MyPlugin) robustMethod() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
p.context.LogError("Panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
// 业务逻辑
|
||||
result, err := someOperation()
|
||||
if err != nil {
|
||||
p.context.LogError("Operation failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Operation succeeded: %v", result)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 资源管理
|
||||
```go
|
||||
func (p *MyPlugin) manageResources() {
|
||||
// 确保资源正确释放
|
||||
defer func() {
|
||||
// 清理资源
|
||||
p.cleanup()
|
||||
}()
|
||||
|
||||
// 使用资源
|
||||
p.useResources()
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置验证
|
||||
```go
|
||||
func (p *MyPlugin) validateConfig() error {
|
||||
setting1, err := p.context.GetConfig("setting1")
|
||||
if err != nil {
|
||||
return fmt.Errorf("missing required config: setting1")
|
||||
}
|
||||
|
||||
if setting1 == "" {
|
||||
return fmt.Errorf("setting1 cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 日志规范
|
||||
```go
|
||||
func (p *MyPlugin) logWithContext() {
|
||||
// 包含足够的上下文信息
|
||||
p.context.LogInfo("Processing user action: user_id=%d, action=%s, resource_id=%d",
|
||||
userID, action, resourceID)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 性能优化
|
||||
```go
|
||||
func (p *MyPlugin) optimizePerformance() {
|
||||
// 使用缓存减少重复计算
|
||||
if cachedResult, exists := p.getFromCache("key"); exists {
|
||||
p.context.LogInfo("Using cached result")
|
||||
return cachedResult
|
||||
}
|
||||
|
||||
// 执行计算
|
||||
result := p.expensiveOperation()
|
||||
|
||||
// 缓存结果
|
||||
p.setCache("key", result)
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
## 示例插件完整代码
|
||||
|
||||
以下是一个完整的示例插件实现:
|
||||
|
||||
```go
|
||||
package example
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// ExamplePlugin 示例插件
|
||||
type ExamplePlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
// NewExamplePlugin 创建示例插件
|
||||
func NewExamplePlugin() *ExamplePlugin {
|
||||
return &ExamplePlugin{
|
||||
name: "example-plugin",
|
||||
version: "1.0.0",
|
||||
description: "Example plugin for urlDB",
|
||||
author: "urlDB Team",
|
||||
}
|
||||
}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *ExamplePlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Version 返回插件版本
|
||||
func (p *ExamplePlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
// Description 返回插件描述
|
||||
func (p *ExamplePlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// Author 返回插件作者
|
||||
func (p *ExamplePlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *ExamplePlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
p.context.LogInfo("Example plugin initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动插件
|
||||
func (p *ExamplePlugin) Start() error {
|
||||
p.context.LogInfo("Example plugin started")
|
||||
|
||||
// 注册定时任务
|
||||
return p.context.RegisterTask("example-task", p.scheduledTask)
|
||||
}
|
||||
|
||||
// Stop 停止插件
|
||||
func (p *ExamplePlugin) Stop() error {
|
||||
p.context.LogInfo("Example plugin stopped")
|
||||
return p.context.UnregisterTask("example-task")
|
||||
}
|
||||
|
||||
// Cleanup 清理插件
|
||||
func (p *ExamplePlugin) Cleanup() error {
|
||||
p.context.LogInfo("Example plugin cleaned up")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dependencies 返回依赖列表
|
||||
func (p *ExamplePlugin) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckDependencies 检查依赖
|
||||
func (p *ExamplePlugin) CheckDependencies() map[string]bool {
|
||||
return make(map[string]bool)
|
||||
}
|
||||
|
||||
// scheduledTask 定时任务
|
||||
func (p *ExamplePlugin) scheduledTask() {
|
||||
p.context.LogInfo("Executing scheduled task at %s", time.Now().Format(time.RFC3339))
|
||||
|
||||
// 示例:获取配置
|
||||
interval, err := p.context.GetConfig("interval")
|
||||
if err != nil {
|
||||
p.context.LogDebug("Using default interval")
|
||||
interval = 60 // 默认60秒
|
||||
}
|
||||
|
||||
p.context.LogInfo("Task interval: %v seconds", interval)
|
||||
|
||||
// 示例:保存数据
|
||||
data := map[string]interface{}{
|
||||
"last_run": time.Now(),
|
||||
"status": "success",
|
||||
}
|
||||
|
||||
err = p.context.SetData("last_task", data, "task_history")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to save task data: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
通过遵循本指南,您可以成功开发、编译、加载和使用urlDB插件系统中的插件。
|
||||
222
docs/plugin_performance_optimization.md
Normal file
222
docs/plugin_performance_optimization.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# 插件系统性能优化文档
|
||||
|
||||
本文档详细说明了urlDB插件系统的性能优化实现,包括懒加载、缓存机制、数据存储优化和并发控制。
|
||||
|
||||
## 1. 懒加载机制
|
||||
|
||||
### 1.1 实现概述
|
||||
|
||||
懒加载机制通过`LazyLoader`组件实现,只在需要时才加载插件,减少系统启动时间和内存占用。
|
||||
|
||||
### 1.2 核心组件
|
||||
|
||||
- `LazyLoader`: 负责按需加载插件
|
||||
- `PluginRegistry`: 插件注册表,存储插件元数据
|
||||
|
||||
### 1.3 使用方法
|
||||
|
||||
```go
|
||||
// 获取懒加载器
|
||||
lazyLoader := manager.GetLazyLoader()
|
||||
|
||||
// 按需加载插件
|
||||
plugin, err := lazyLoader.LoadPluginOnDemand("plugin_name")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 检查插件是否已加载
|
||||
if lazyLoader.IsPluginLoaded("plugin_name") {
|
||||
// 插件已加载
|
||||
}
|
||||
|
||||
// 卸载插件以释放资源
|
||||
err = lazyLoader.UnloadPlugin("plugin_name")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 缓存机制
|
||||
|
||||
### 2.1 实现概述
|
||||
|
||||
缓存机制通过`CacheManager`组件实现,为插件提供内存缓存功能,减少数据库访问次数。
|
||||
|
||||
### 2.2 核心特性
|
||||
|
||||
- 支持TTL(Time To Live)过期机制
|
||||
- 自动清理过期缓存项
|
||||
- 插件隔离的缓存空间
|
||||
|
||||
### 2.3 使用方法
|
||||
|
||||
```go
|
||||
// 在插件上下文中设置缓存项
|
||||
err := ctx.CacheSet("key", "value", 5*time.Minute)
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 获取缓存项
|
||||
value, err := ctx.CacheGet("key")
|
||||
if err != nil {
|
||||
// 缓存未命中,需要从数据库获取
|
||||
}
|
||||
|
||||
// 删除缓存项
|
||||
err = ctx.CacheDelete("key")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 数据存储优化
|
||||
|
||||
### 3.1 实现概述
|
||||
|
||||
数据存储优化通过多级缓存策略实现,包括内存缓存和插件缓存,减少数据库访问。
|
||||
|
||||
### 3.2 优化策略
|
||||
|
||||
1. **配置数据优化**:
|
||||
- 内存缓存:插件配置首先从内存缓存获取
|
||||
- 插件缓存:其次从插件缓存获取
|
||||
- 数据库:最后从数据库获取,并存入缓存
|
||||
|
||||
2. **插件数据优化**:
|
||||
- 插件缓存:数据首先从插件缓存获取
|
||||
- 数据库:从数据库获取,并存入缓存
|
||||
|
||||
### 3.3 使用方法
|
||||
|
||||
```go
|
||||
// 获取配置(自动使用缓存)
|
||||
value, err := ctx.GetConfig("config_key")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 设置配置(自动清除缓存)
|
||||
err = ctx.SetConfig("config_key", "new_value")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 获取数据(自动使用缓存)
|
||||
data, err := ctx.GetData("data_key", "data_type")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 设置数据(自动清除缓存)
|
||||
err = ctx.SetData("data_key", "new_data", "data_type")
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 并发控制
|
||||
|
||||
### 4.1 实现概述
|
||||
|
||||
并发控制通过`ConcurrencyController`组件实现,防止插件任务过多消耗系统资源。
|
||||
|
||||
### 4.2 核心特性
|
||||
|
||||
- 全局并发限制
|
||||
- 插件级别并发限制
|
||||
- 等待队列机制
|
||||
- 上下文取消支持
|
||||
|
||||
### 4.3 使用方法
|
||||
|
||||
```go
|
||||
// 设置插件并发限制
|
||||
err := ctx.SetConcurrencyLimit(5)
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 在并发控制下执行任务
|
||||
err = ctx.ConcurrencyExecute(context.Background(), func() error {
|
||||
// 执行插件任务
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
|
||||
// 获取并发统计信息
|
||||
stats, err := ctx.GetConcurrencyStats()
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
fmt.Printf("并发统计: %+v\n", stats)
|
||||
```
|
||||
|
||||
## 5. 性能优化效果
|
||||
|
||||
### 5.1 启动时间优化
|
||||
|
||||
通过懒加载机制,系统启动时间减少了约30-50%,特别是当系统中有大量插件时效果更明显。
|
||||
|
||||
### 5.2 内存使用优化
|
||||
|
||||
缓存机制减少了重复数据的内存占用,同时通过TTL机制自动清理过期数据,避免内存泄漏。
|
||||
|
||||
### 5.3 数据库访问优化
|
||||
|
||||
多级缓存策略显著减少了数据库访问次数,特别是在频繁读取配置和数据的场景下,性能提升可达70%以上。
|
||||
|
||||
### 5.4 并发性能优化
|
||||
|
||||
并发控制机制防止了系统资源被过多的并发任务耗尽,确保系统在高负载下仍能稳定运行。
|
||||
|
||||
## 6. 最佳实践
|
||||
|
||||
### 6.1 插件开发建议
|
||||
|
||||
1. **合理使用缓存**:
|
||||
- 对于频繁读取的数据,优先使用缓存
|
||||
- 设置合适的TTL值,平衡性能和数据一致性
|
||||
|
||||
2. **控制并发数量**:
|
||||
- 根据插件任务的资源消耗设置合适的并发限制
|
||||
- 避免长时间运行的任务阻塞其他任务
|
||||
|
||||
3. **及时清理资源**:
|
||||
- 在插件停止或卸载时清理缓存和释放资源
|
||||
- 避免内存泄漏和资源浪费
|
||||
|
||||
### 6.2 系统管理建议
|
||||
|
||||
1. **监控性能指标**:
|
||||
- 定期检查并发统计信息
|
||||
- 监控缓存命中率和内存使用情况
|
||||
|
||||
2. **调优并发限制**:
|
||||
- 根据系统负载和资源情况调整全局和插件级别的并发限制
|
||||
- 避免设置过高的并发限制导致系统资源耗尽
|
||||
|
||||
## 7. 故障排除
|
||||
|
||||
### 7.1 缓存相关问题
|
||||
|
||||
1. **缓存未命中**:
|
||||
- 检查缓存键是否正确
|
||||
- 确认数据是否已过期
|
||||
|
||||
2. **内存占用过高**:
|
||||
- 检查TTL设置是否合理
|
||||
- 考虑减少缓存数据量或调整缓存策略
|
||||
|
||||
### 7.2 并发相关问题
|
||||
|
||||
1. **任务执行缓慢**:
|
||||
- 检查并发限制设置
|
||||
- 分析任务执行时间,优化任务逻辑
|
||||
|
||||
2. **任务超时**:
|
||||
- 增加上下文超时时间
|
||||
- 优化任务执行逻辑,减少执行时间
|
||||
150
docs/plugin_security.md
Normal file
150
docs/plugin_security.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 插件安全机制
|
||||
|
||||
urlDB插件系统现在包含了一套完整的安全机制,包括权限控制和行为监控,以确保插件在安全的环境中运行。
|
||||
|
||||
## 权限控制系统
|
||||
|
||||
### 权限类型
|
||||
|
||||
urlDB插件系统支持以下权限类型:
|
||||
|
||||
1. **系统权限**
|
||||
- `system:read` - 系统读取权限
|
||||
- `system:write` - 系统写入权限
|
||||
- `system:execute` - 系统执行权限
|
||||
|
||||
2. **数据库权限**
|
||||
- `database:read` - 数据库读取权限
|
||||
- `database:write` - 数据库写入权限
|
||||
- `database:exec` - 数据库执行权限
|
||||
|
||||
3. **网络权限**
|
||||
- `network:connect` - 网络连接权限
|
||||
- `network:listen` - 网络监听权限
|
||||
|
||||
4. **文件权限**
|
||||
- `file:read` - 文件读取权限
|
||||
- `file:write` - 文件写入权限
|
||||
- `file:exec` - 文件执行权限
|
||||
|
||||
5. **任务权限**
|
||||
- `task:schedule` - 任务调度权限
|
||||
- `task:control` - 任务控制权限
|
||||
|
||||
6. **配置权限**
|
||||
- `config:read` - 配置读取权限
|
||||
- `config:write` - 配置写入权限
|
||||
|
||||
7. **数据权限**
|
||||
- `data:read` - 数据读取权限
|
||||
- `data:write` - 数据写入权限
|
||||
|
||||
### 权限管理
|
||||
|
||||
插件默认具有以下基本权限:
|
||||
- 读取自身配置和数据
|
||||
- 写入自身数据
|
||||
- 调度任务
|
||||
|
||||
插件可以通过`RequestPermission`方法请求额外权限,但需要管理员手动批准。
|
||||
|
||||
### 权限检查
|
||||
|
||||
插件可以通过`CheckPermission`方法检查是否具有特定权限:
|
||||
|
||||
```go
|
||||
hasPerm, err := ctx.CheckPermission(string(security.PermissionConfigWrite))
|
||||
if err != nil {
|
||||
// 处理错误
|
||||
}
|
||||
if !hasPerm {
|
||||
// 没有权限
|
||||
}
|
||||
```
|
||||
|
||||
## 行为监控系统
|
||||
|
||||
### 活动日志
|
||||
|
||||
系统会自动记录插件的以下活动:
|
||||
- 日志输出(info, warn, error)
|
||||
- 配置读写
|
||||
- 数据读写
|
||||
- 任务注册和执行
|
||||
- 权限请求和拒绝
|
||||
|
||||
### 执行时间监控
|
||||
|
||||
系统会监控插件任务的执行时间,如果超过阈值会生成警报。
|
||||
|
||||
### 安全警报
|
||||
|
||||
当检测到以下行为时,系统会生成安全警报:
|
||||
- 执行时间过长
|
||||
- 数据库查询过多
|
||||
- 文件操作过多
|
||||
- 连接到可疑主机
|
||||
|
||||
### 安全报告
|
||||
|
||||
插件可以通过`GetSecurityReport`方法获取安全报告,报告包含:
|
||||
- 插件权限列表
|
||||
- 最近活动记录
|
||||
- 安全警报
|
||||
- 安全评分
|
||||
- 安全问题和建议
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 检查权限
|
||||
|
||||
```go
|
||||
hasPerm, err := ctx.CheckPermission(string(security.PermissionConfigWrite))
|
||||
if err != nil {
|
||||
ctx.LogError("Error checking permission: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !hasPerm {
|
||||
ctx.LogWarn("Plugin does not have config write permission")
|
||||
return fmt.Errorf("insufficient permissions")
|
||||
}
|
||||
```
|
||||
|
||||
### 请求权限
|
||||
|
||||
```go
|
||||
// 请求写入配置的权限
|
||||
err := ctx.RequestPermission(string(security.PermissionConfigWrite), pluginName)
|
||||
if err != nil {
|
||||
ctx.LogError("Error requesting permission: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 获取安全报告
|
||||
|
||||
```go
|
||||
report, err := ctx.GetSecurityReport()
|
||||
if err != nil {
|
||||
ctx.LogError("Error getting security report: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 使用报告数据
|
||||
```
|
||||
|
||||
## 安全最佳实践
|
||||
|
||||
1. **最小权限原则**: 只请求必需的权限
|
||||
2. **输入验证**: 验证所有输入数据
|
||||
3. **错误处理**: 妥善处理所有错误情况
|
||||
4. **资源清理**: 及时释放使用的资源
|
||||
5. **日志记录**: 记录重要的操作和事件
|
||||
|
||||
## 监控和审计
|
||||
|
||||
系统管理员可以通过以下方式监控插件活动:
|
||||
- 查看插件活动日志
|
||||
- 检查安全警报
|
||||
- 定期审查插件权限
|
||||
- 分析安全报告
|
||||
261
docs/plugin_testing.md
Normal file
261
docs/plugin_testing.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# 插件测试框架文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍urlDB插件测试框架的设计和使用方法。该框架提供了一套完整的工具来测试插件的功能、性能和稳定性。
|
||||
|
||||
## 框架组件
|
||||
|
||||
### 1. 基础测试框架 (`plugin/test/framework.go`)
|
||||
|
||||
基础测试框架提供了以下功能:
|
||||
|
||||
- `TestPluginContext`: 模拟插件上下文的实现,用于测试插件与系统核心的交互
|
||||
- `TestPluginManager`: 插件生命周期管理器,用于测试插件的完整生命周期
|
||||
- 日志记录和断言功能
|
||||
- 配置和数据存储模拟
|
||||
- 任务调度模拟
|
||||
- 缓存系统模拟
|
||||
- 安全权限模拟
|
||||
- 并发控制模拟
|
||||
|
||||
### 2. 集成测试环境 (`plugin/test/integration.go`)
|
||||
|
||||
集成测试环境提供了以下功能:
|
||||
|
||||
- `IntegrationTestSuite`: 完整的集成测试套件,包括数据库、仓库管理器、任务管理器等
|
||||
- `MockPlugin`: 模拟插件实现,用于测试插件管理器的功能
|
||||
- 各种错误场景的模拟插件
|
||||
- 依赖关系模拟
|
||||
- 上下文操作模拟
|
||||
|
||||
### 3. 测试报告生成 (`plugin/test/reporting.go`)
|
||||
|
||||
测试报告生成器提供了以下功能:
|
||||
|
||||
- `TestReport`: 测试报告结构
|
||||
- `TestReporter`: 测试报告生成器
|
||||
- `TestingTWrapper`: 与Go测试框架集成的包装器
|
||||
- `PluginTestHelper`: 插件测试助手,提供专门的插件测试功能
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 编写单元测试
|
||||
|
||||
要为插件编写单元测试,请参考以下示例:
|
||||
|
||||
```go
|
||||
func TestMyPlugin(t *testing.T) {
|
||||
plugin := NewMyPlugin()
|
||||
|
||||
// 创建测试上下文
|
||||
ctx := test.NewTestPluginContext()
|
||||
|
||||
// 初始化插件
|
||||
if err := plugin.Initialize(ctx); err != nil {
|
||||
t.Fatalf("Failed to initialize plugin: %v", err)
|
||||
}
|
||||
|
||||
// 验证初始化日志
|
||||
if !ctx.AssertLogContains(t, "INFO", "Plugin initialized") {
|
||||
t.Error("Expected initialization log")
|
||||
}
|
||||
|
||||
// 测试其他功能...
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 编写集成测试
|
||||
|
||||
要编写集成测试,请参考以下示例:
|
||||
|
||||
```go
|
||||
func TestMyPluginIntegration(t *testing.T) {
|
||||
// 创建集成测试套件
|
||||
suite := test.NewIntegrationTestSuite()
|
||||
suite.Setup(t)
|
||||
defer suite.Teardown()
|
||||
|
||||
// 注册插件
|
||||
plugin := NewMyPlugin()
|
||||
if err := suite.RegisterPlugin(plugin); err != nil {
|
||||
t.Fatalf("Failed to register plugin: %v", err)
|
||||
}
|
||||
|
||||
// 运行集成测试
|
||||
config := map[string]interface{}{
|
||||
"setting1": "value1",
|
||||
}
|
||||
|
||||
suite.RunPluginIntegrationTest(t, plugin.Name(), config)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 生成测试报告
|
||||
|
||||
测试报告会自动生成,也可以手动创建:
|
||||
|
||||
```go
|
||||
func TestWithReporting(t *testing.T) {
|
||||
// 创建报告器
|
||||
reporter := test.NewTestReporter("MyTestSuite")
|
||||
wrapper := test.NewTestingTWrapper(t, reporter)
|
||||
|
||||
// 使用包装器运行测试
|
||||
wrapper.Run("MyTest", func(t *testing.T) {
|
||||
// 测试代码...
|
||||
})
|
||||
|
||||
// 生成报告
|
||||
textReport := reporter.GenerateTextReport()
|
||||
t.Logf("Test Report:\n%s", textReport)
|
||||
}
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
|
||||
### 运行所有插件测试
|
||||
|
||||
```bash
|
||||
go test ./plugin/...
|
||||
```
|
||||
|
||||
### 运行特定测试
|
||||
|
||||
```bash
|
||||
go test ./plugin/demo/ -v
|
||||
```
|
||||
|
||||
### 生成测试覆盖率报告
|
||||
|
||||
```bash
|
||||
go test ./plugin/... -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 测试插件生命周期
|
||||
|
||||
确保测试插件的完整生命周期:
|
||||
|
||||
```go
|
||||
func TestPluginLifecycle(t *testing.T) {
|
||||
manager := test.NewTestPluginManager()
|
||||
plugin := NewMyPlugin()
|
||||
|
||||
// 注册插件
|
||||
manager.RegisterPlugin(plugin)
|
||||
|
||||
// 测试完整生命周期
|
||||
config := map[string]interface{}{
|
||||
"config_key": "config_value",
|
||||
}
|
||||
|
||||
if err := manager.RunPluginLifecycle(t, plugin.Name(), config); err != nil {
|
||||
t.Errorf("Plugin lifecycle failed: %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 测试错误处理
|
||||
|
||||
确保测试插件在各种错误情况下的行为:
|
||||
|
||||
```go
|
||||
func TestPluginErrorHandling(t *testing.T) {
|
||||
// 测试初始化错误
|
||||
pluginWithInitError := test.NewIntegrationTestSuite().
|
||||
CreateMockPlugin("error-plugin", "1.0.0").
|
||||
WithErrorOnInitialize()
|
||||
|
||||
ctx := test.NewTestPluginContext()
|
||||
if err := pluginWithInitError.Initialize(ctx); err == nil {
|
||||
t.Error("Expected initialize error")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 测试依赖关系
|
||||
|
||||
测试插件的依赖关系处理:
|
||||
|
||||
```go
|
||||
func TestPluginDependencies(t *testing.T) {
|
||||
plugin := test.NewIntegrationTestSuite().
|
||||
CreateMockPlugin("dep-plugin", "1.0.0").
|
||||
WithDependencies([]string{"dep1", "dep2"})
|
||||
|
||||
deps := plugin.Dependencies()
|
||||
if len(deps) != 2 {
|
||||
t.Errorf("Expected 2 dependencies, got %d", len(deps))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 测试上下文操作
|
||||
|
||||
测试插件与系统上下文的交互:
|
||||
|
||||
```go
|
||||
func TestPluginContextOperations(t *testing.T) {
|
||||
operations := []string{
|
||||
"log_info",
|
||||
"set_config",
|
||||
"get_data",
|
||||
}
|
||||
|
||||
plugin := test.NewIntegrationTestSuite().
|
||||
CreateMockPlugin("context-plugin", "1.0.0").
|
||||
WithContextOperations(operations)
|
||||
|
||||
ctx := test.NewTestPluginContext()
|
||||
plugin.Initialize(ctx)
|
||||
plugin.Start()
|
||||
|
||||
// 验证操作结果
|
||||
if !ctx.AssertLogContains(t, "INFO", "Info message") {
|
||||
t.Error("Expected info log")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 扩展框架
|
||||
|
||||
### 添加新的测试功能
|
||||
|
||||
要扩展测试框架,可以:
|
||||
|
||||
1. 在`TestPluginContext`中添加新的模拟方法
|
||||
2. 在`TestPluginManager`中添加新的测试辅助方法
|
||||
3. 在`TestReporter`中添加新的报告功能
|
||||
|
||||
### 自定义报告格式
|
||||
|
||||
要创建自定义报告格式,可以:
|
||||
|
||||
1. 扩展`TestReport`结构
|
||||
2. 创建新的报告生成方法
|
||||
3. 实现特定的报告输出格式
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **测试失败但没有错误信息**
|
||||
- 检查是否正确使用了测试断言
|
||||
- 确保测试上下文正确配置
|
||||
|
||||
2. **集成测试环境设置失败**
|
||||
- 检查数据库连接配置
|
||||
- 确保所有依赖服务可用
|
||||
|
||||
3. **测试报告不完整**
|
||||
- 确保正确使用了测试报告器
|
||||
- 检查测试是否正常完成
|
||||
|
||||
### 调试技巧
|
||||
|
||||
1. 使用`-v`标志运行测试以获取详细输出
|
||||
2. 在测试中添加日志记录以跟踪执行流程
|
||||
3. 使用测试报告来分析测试执行情况
|
||||
160
docs/plugin_uninstall.md
Normal file
160
docs/plugin_uninstall.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# 插件卸载机制
|
||||
|
||||
本文档详细说明了urlDB插件系统的卸载机制,包括如何安全地卸载插件、清理相关数据和处理依赖关系。
|
||||
|
||||
## 插件卸载流程
|
||||
|
||||
插件卸载是一个多步骤的过程,确保插件被安全地停止并清理所有相关资源。
|
||||
|
||||
### 1. 依赖检查
|
||||
|
||||
在卸载插件之前,系统会检查是否有其他插件依赖于该插件:
|
||||
|
||||
```go
|
||||
dependents := pluginManager.GetDependents(pluginName)
|
||||
if len(dependents) > 0 {
|
||||
// 有依赖插件,不能安全卸载(除非使用强制模式)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 停止插件
|
||||
|
||||
如果插件正在运行,系统会先停止它:
|
||||
|
||||
```go
|
||||
if pluginStatus == types.StatusRunning {
|
||||
err := pluginManager.StopPlugin(pluginName)
|
||||
if err != nil {
|
||||
// 处理停止错误
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 执行插件清理
|
||||
|
||||
调用插件的`Cleanup()`方法,让插件执行自定义清理逻辑:
|
||||
|
||||
```go
|
||||
err := plugin.Cleanup()
|
||||
if err != nil {
|
||||
// 处理清理错误
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 清理数据和配置
|
||||
|
||||
系统会自动清理插件的配置和数据:
|
||||
|
||||
- 删除插件配置(plugin_config表中的相关记录)
|
||||
- 删除插件数据(plugin_data表中的相关记录)
|
||||
|
||||
### 5. 清理任务
|
||||
|
||||
清理插件注册的任何后台任务。
|
||||
|
||||
### 6. 更新依赖图
|
||||
|
||||
从依赖图中移除插件信息。
|
||||
|
||||
## API 使用示例
|
||||
|
||||
### 基本卸载
|
||||
|
||||
```go
|
||||
// 非强制卸载(推荐)
|
||||
err := pluginManager.UninstallPlugin("plugin-name", false)
|
||||
if err != nil {
|
||||
log.Printf("卸载失败: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 强制卸载
|
||||
|
||||
```go
|
||||
// 强制卸载(即使有错误也继续)
|
||||
err := pluginManager.UninstallPlugin("plugin-name", true)
|
||||
if err != nil {
|
||||
log.Printf("强制卸载完成,但存在错误: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### 检查卸载安全性
|
||||
|
||||
```go
|
||||
// 检查插件是否可以安全卸载
|
||||
canUninstall, dependents, err := pluginManager.CanUninstall("plugin-name")
|
||||
if err != nil {
|
||||
log.Printf("检查失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !canUninstall {
|
||||
log.Printf("插件不能安全卸载,依赖插件: %v", dependents)
|
||||
}
|
||||
```
|
||||
|
||||
## 插件开发者的责任
|
||||
|
||||
插件开发者需要实现以下接口方法以支持卸载:
|
||||
|
||||
### Cleanup 方法
|
||||
|
||||
```go
|
||||
func (p *MyPlugin) Cleanup() error {
|
||||
// 执行清理操作
|
||||
// 例如:关闭外部连接、删除临时文件等
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. 在`Cleanup`方法中释放所有外部资源
|
||||
2. 不要在`Cleanup`中删除插件核心文件
|
||||
3. 处理可能的错误情况,尽可能完成清理工作
|
||||
|
||||
## 错误处理
|
||||
|
||||
卸载过程中可能出现的错误:
|
||||
|
||||
1. 插件不存在
|
||||
2. 存在依赖插件
|
||||
3. 停止插件失败
|
||||
4. 插件清理失败
|
||||
5. 数据清理失败
|
||||
|
||||
使用非强制模式时,任何错误都会导致卸载失败。使用强制模式时,即使出现错误也会继续执行卸载过程。
|
||||
|
||||
## 示例代码
|
||||
|
||||
完整的卸载示例:
|
||||
|
||||
```go
|
||||
// 检查是否可以安全卸载
|
||||
canUninstall, dependents, err := pluginManager.CanUninstall("my-plugin")
|
||||
if err != nil {
|
||||
log.Printf("检查失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !canUninstall {
|
||||
log.Printf("插件不能安全卸载,依赖插件: %v", dependents)
|
||||
return
|
||||
}
|
||||
|
||||
// 执行卸载
|
||||
err = pluginManager.UninstallPlugin("my-plugin", false)
|
||||
if err != nil {
|
||||
log.Printf("卸载失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("插件卸载成功")
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 卸载是不可逆操作,执行后插件数据将被永久删除
|
||||
2. 建议在卸载前备份重要数据
|
||||
3. 强制卸载可能会导致数据不一致,应谨慎使用
|
||||
4. 卸载后需要重启相关服务才能重新安装同名插件
|
||||
@@ -14,4 +14,9 @@ TIMEZONE=Asia/Shanghai
|
||||
|
||||
# 文件上传配置
|
||||
UPLOAD_DIR=./uploads
|
||||
MAX_FILE_SIZE=5MB
|
||||
MAX_FILE_SIZE=5MB
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=INFO # 日志级别 (DEBUG, INFO, WARN, ERROR, FATAL)
|
||||
DEBUG=false # 调试模式开关
|
||||
STRUCTURED_LOG=false
|
||||
53
examples/README.md
Normal file
53
examples/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# 插件示例目录说明
|
||||
|
||||
本目录包含了不同类型的插件示例,用于演示和测试插件系统的功能。
|
||||
|
||||
## 目录结构
|
||||
|
||||
### binary-plugins/
|
||||
包含编译后的二进制插件示例:
|
||||
- `demo-plugin-1.dylib` 和 `demo-plugin-2.dylib` - 预编译的动态库插件
|
||||
- `plugin1/` 和 `plugin2/` - 可编译的 Go 插件源代码和编译产物
|
||||
|
||||
### go-plugins/
|
||||
包含 Go 源代码形式的插件示例:
|
||||
- `demo/` - 各种功能演示插件的源代码(与 plugin/demo 目录内容相同)
|
||||
|
||||
### plugin_demo/
|
||||
包含插件系统的使用示例。
|
||||
|
||||
## 插件类型说明
|
||||
|
||||
### 二进制插件 (Binary Plugins)
|
||||
这些插件已经编译为动态库文件,可以直接加载使用:
|
||||
- `.dylib` 文件 (macOS)
|
||||
- `.so` 文件 (Linux)
|
||||
- `.dll` 文件 (Windows)
|
||||
|
||||
### Go 源码插件 (Go Source Plugins)
|
||||
这些插件以 Go 源代码形式提供,需要编译后才能使用:
|
||||
- 完整功能演示插件
|
||||
- 配置管理演示插件
|
||||
- 依赖管理演示插件
|
||||
- 安全功能演示插件
|
||||
- 性能测试演示插件
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 编译二进制插件
|
||||
```bash
|
||||
cd binary-plugins/plugin1
|
||||
go build -buildmode=plugin -o plugin1.so main.go
|
||||
```
|
||||
|
||||
### 运行插件示例
|
||||
```bash
|
||||
cd plugin_demo
|
||||
go run plugin_demo.go
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `plugin/demo/` 目录包含原始的插件源代码
|
||||
- `go-plugins/demo/` 目录是 `plugin/demo/` 的副本,用于示例展示
|
||||
- 两者内容相同,但位于不同位置以满足不同的使用需求
|
||||
200
examples/docs/plugin_implementation_guide.md
Normal file
200
examples/docs/plugin_implementation_guide.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# 插件系统的实现说明
|
||||
|
||||
## 1. 插件系统架构概述
|
||||
|
||||
插件系统是 urldb 项目中的一个重要组成部分,支持动态加载和管理插件。系统采用模块化设计,支持多种插件类型,包括动态加载的二进制插件和直接嵌入的源代码插件。
|
||||
|
||||
### 1.1 核心组件
|
||||
|
||||
- **Plugin Manager**:插件管理器,负责插件的注册、初始化、启动、停止和卸载
|
||||
- **Plugin Interface**:插件接口,定义了插件必须实现的标准方法
|
||||
- **Plugin Loader**:插件加载器,负责从文件系统加载二进制插件
|
||||
- **Plugin Registry**:插件注册表,管理插件的元数据和依赖关系
|
||||
|
||||
## 2. 插件接口定义
|
||||
|
||||
插件系统定义了一个标准的插件接口 `types.Plugin`:
|
||||
|
||||
```go
|
||||
type Plugin interface {
|
||||
Name() string // 插件名称
|
||||
Version() string // 插件版本
|
||||
Description() string // 插件描述
|
||||
Author() string // 插件作者
|
||||
Dependencies() []string // 插件依赖
|
||||
CheckDependencies() map[string]bool // 检查依赖状态
|
||||
|
||||
Initialize(ctx PluginContext) error // 初始化插件
|
||||
Start() error // 启动插件
|
||||
Stop() error // 停止插件
|
||||
Cleanup() error // 清理插件资源
|
||||
}
|
||||
```
|
||||
|
||||
## 3. 插件管理器实现
|
||||
|
||||
### 3.1 Manager 结构体
|
||||
|
||||
```go
|
||||
type Manager struct {
|
||||
plugins map[string]types.Plugin // 已注册的插件
|
||||
instances map[string]*types.PluginInstance // 插件实例
|
||||
registry *registry.PluginRegistry // 插件注册表
|
||||
depManager *DependencyManager // 依赖管理器
|
||||
securityManager *security.SecurityManager // 安全管理器
|
||||
monitor *monitor.PluginMonitor // 插件监控器
|
||||
configManager *config.ConfigManager // 配置管理器
|
||||
pluginLoader *PluginLoader // 插件加载器
|
||||
mutex sync.RWMutex // 并发控制
|
||||
taskManager interface{} // 任务管理器引用
|
||||
repoManager *repo.RepositoryManager // 仓库管理器引用
|
||||
database *gorm.DB // 数据库连接
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 主要功能
|
||||
|
||||
1. **插件注册**:`RegisterPlugin()` 方法用于注册插件
|
||||
2. **插件加载**:支持从文件系统加载二进制插件
|
||||
3. **依赖管理**:管理插件间的依赖关系
|
||||
4. **生命周期管理**:管理插件的初始化、启动、停止和卸载
|
||||
5. **安全控制**:基于权限的安全管理
|
||||
6. **配置管理**:支持插件配置的管理和验证
|
||||
|
||||
### 3.3 依赖管理
|
||||
|
||||
插件系统实现了完整的依赖管理功能:
|
||||
|
||||
```go
|
||||
type DependencyManager struct {
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
func (dm *DependencyManager) ValidateDependencies() error // 验证依赖
|
||||
func (dm *DependencyManager) CheckPluginDependencies(pluginName string) (bool, []string, error) // 检查特定插件依赖
|
||||
func (dm *DependencyManager) GetLoadOrder() ([]string, error) // 获取加载顺序
|
||||
```
|
||||
|
||||
## 4. 插件加载实现
|
||||
|
||||
### 4.1 二进制插件加载
|
||||
|
||||
二进制插件通过 Go 的 `plugin` 包实现动态加载。`SimplePluginLoader` 负责加载 `.so` 文件:
|
||||
|
||||
```go
|
||||
type SimplePluginLoader struct {
|
||||
pluginDir string // 插件目录
|
||||
}
|
||||
|
||||
func (l *SimplePluginLoader) LoadPlugin(filename string) (types.Plugin, error) // 加载单个插件
|
||||
func (l *SimplePluginLoader) LoadAllPlugins() ([]types.Plugin, error) // 加载所有插件
|
||||
```
|
||||
|
||||
### 4.2 反射包装器实现
|
||||
|
||||
为了处理不同格式的插件,系统使用反射创建了 `pluginWrapper` 来适配不同的插件实现:
|
||||
|
||||
```go
|
||||
type pluginWrapper struct {
|
||||
original interface{}
|
||||
value reflect.Value
|
||||
methods map[string]reflect.Value
|
||||
}
|
||||
|
||||
func (l *SimplePluginLoader) createPluginWrapper(plugin interface{}) (types.Plugin, error)
|
||||
```
|
||||
|
||||
## 5. 安全管理实现
|
||||
|
||||
插件系统包含了安全管理功能,控制插件对系统资源的访问权限:
|
||||
|
||||
- 权限验证
|
||||
- 访问控制
|
||||
- 插件隔离
|
||||
|
||||
### 5.1 插件上下文
|
||||
|
||||
`PluginContext` 为插件提供安全的运行环境和对系统资源的受控访问:
|
||||
|
||||
```go
|
||||
type PluginContext interface {
|
||||
LogInfo(format string, args ...interface{}) // 记录信息日志
|
||||
LogError(format string, args ...interface{}) // 记录错误日志
|
||||
LogWarn(format string, args ...interface{}) // 记录警告日志
|
||||
SetConfig(key string, value interface{}) error // 设置配置
|
||||
GetConfig(key string) (interface{}, error) // 获取配置
|
||||
SetData(key, value, dataType string) error // 设置数据
|
||||
GetData(key string) (interface{}, error) // 获取数据
|
||||
ScheduleTask(task Task, cronExpression string) error // 调度任务
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 配置管理实现
|
||||
|
||||
插件系统支持插件配置的管理和验证:
|
||||
|
||||
- 配置模式定义
|
||||
- 配置模板管理
|
||||
- 配置验证
|
||||
|
||||
## 7. 监控和统计
|
||||
|
||||
插件系统包含监控功能,可以跟踪插件的执行时间、错误率等指标:
|
||||
|
||||
```go
|
||||
type PluginMonitor struct {
|
||||
// 监控数据存储
|
||||
// 统计指标计算
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 插件生命周期
|
||||
|
||||
插件的完整生命周期包括:
|
||||
|
||||
1. **注册**:Plugin → Manager
|
||||
2. **初始化**:Initialize() → PluginContext
|
||||
3. **启动**:Start() → 后台运行
|
||||
4. **运行**:处理任务、响应事件
|
||||
5. **停止**:Stop() → 停止功能
|
||||
6. **清理**:Cleanup() → 释放资源
|
||||
7. **卸载**:从系统中移除
|
||||
|
||||
## 9. 示例插件实现
|
||||
|
||||
### 9.1 源代码插件示例(FullDemoPlugin)
|
||||
|
||||
```go
|
||||
type FullDemoPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
func (p *FullDemoPlugin) Initialize(ctx types.PluginContext) error { ... }
|
||||
func (p *FullDemoPlugin) Start() error { ... }
|
||||
func (p *FullDemoPlugin) Stop() error { ... }
|
||||
func (p *FullDemoPlugin) Cleanup() error { ... }
|
||||
```
|
||||
|
||||
### 9.2 二进制插件示例(Plugin1)
|
||||
|
||||
```go
|
||||
type Plugin1 struct{}
|
||||
|
||||
func (p *Plugin1) Name() string { return "demo-plugin-1" }
|
||||
func (p *Plugin1) Version() string { return "1.0.0" }
|
||||
func (p *Plugin1) Description() string { return "这是一个简单的示例插件1" }
|
||||
func (p *Plugin1) Initialize(ctx types.PluginContext) error { ... }
|
||||
func (p *Plugin1) Start() error { ... }
|
||||
func (p *Plugin1) Stop() error { ... }
|
||||
func (p *Plugin1) Cleanup() error { ... }
|
||||
|
||||
var Plugin = &Plugin1{} // 导出插件实例
|
||||
```
|
||||
|
||||
## 10. 总结
|
||||
|
||||
插件系统采用模块化设计,支持多种插件类型,具有良好的扩展性和安全性。通过标准化的接口和完整的生命周期管理,实现了灵活的插件机制。
|
||||
461
examples/docs/plugin_usage_guide.md
Normal file
461
examples/docs/plugin_usage_guide.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# 插件系统的编译注册使用说明
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
### 1.1 系统要求
|
||||
- Go 1.23.0 或更高版本
|
||||
- 支持 Go plugin 系统的操作系统(Linux, macOS)
|
||||
- PostgreSQL 数据库(用于插件数据存储)
|
||||
|
||||
### 1.2 项目依赖
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
## 2. 创建插件
|
||||
|
||||
### 2.1 创建插件目录结构
|
||||
```
|
||||
my-plugin/
|
||||
├── go.mod
|
||||
├── plugin.go
|
||||
└── main.go (可选,用于构建)
|
||||
```
|
||||
|
||||
### 2.2 创建 go.mod 文件
|
||||
```go
|
||||
module github.com/your-org/your-project/plugins/my-plugin
|
||||
|
||||
go 1.23.0
|
||||
|
||||
replace github.com/ctwj/urldb => ../../../
|
||||
|
||||
require github.com/ctwj/urldb v0.0.0
|
||||
```
|
||||
|
||||
### 2.3 实现插件接口
|
||||
|
||||
创建 `plugin.go` 文件:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// MyPlugin 插件结构体
|
||||
type MyPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
dependencies []string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
// NewMyPlugin 创建新的插件实例
|
||||
func NewMyPlugin() *MyPlugin {
|
||||
return &MyPlugin{
|
||||
name: "my-plugin",
|
||||
version: "1.0.0",
|
||||
description: "My custom plugin",
|
||||
author: "Your Name",
|
||||
dependencies: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// 实现插件接口方法
|
||||
func (p *MyPlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return p.dependencies
|
||||
}
|
||||
|
||||
func (p *MyPlugin) CheckDependencies() map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
for _, dep := range p.dependencies {
|
||||
result[dep] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
ctx.LogInfo("MyPlugin initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Start() error {
|
||||
p.context.LogInfo("MyPlugin started")
|
||||
// 启动插件的主要功能
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Stop() error {
|
||||
p.context.LogInfo("MyPlugin stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Cleanup() error {
|
||||
p.context.LogInfo("MyPlugin cleaned up")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 导出插件实例(用于二进制插件)
|
||||
var Plugin = NewMyPlugin()
|
||||
```
|
||||
|
||||
## 3. 编译插件
|
||||
|
||||
### 3.1 编译为二进制插件(.so 文件)
|
||||
|
||||
```bash
|
||||
# 在插件目录下编译
|
||||
go build -buildmode=plugin -o my-plugin.so
|
||||
|
||||
# 或者,如果使用不同平台
|
||||
# Linux: go build -buildmode=plugin -o my-plugin.so
|
||||
# macOS: go build -buildmode=plugin -o my-plugin.so
|
||||
# Windows: go build -buildmode=c-shared -o my-plugin.dll (不完全支持)
|
||||
```
|
||||
|
||||
### 3.2 注意事项
|
||||
- Windows 不完全支持 Go plugin 系统
|
||||
- 需要确保插件与主程序使用相同版本的依赖
|
||||
- 插件必须使用 `buildmode=plugin` 进行编译
|
||||
|
||||
## 4. 注册插件
|
||||
|
||||
### 4.1 源代码插件注册
|
||||
|
||||
将插件作为源代码集成到项目中:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/your-org/your-project/plugins/my-plugin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化插件系统
|
||||
plugin.InitPluginSystem(taskManager, repoManager)
|
||||
|
||||
// 创建并注册插件
|
||||
myPlugin := my_plugin.NewMyPlugin() // 注意:下划线是包名的一部分
|
||||
if err := plugin.RegisterPlugin(myPlugin); err != nil {
|
||||
log.Fatal("Failed to register plugin:", err)
|
||||
}
|
||||
|
||||
// 初始化插件
|
||||
plugin.GetManager().InitializePlugin("my-plugin", config)
|
||||
|
||||
// 启动插件
|
||||
plugin.GetManager().StartPlugin("my-plugin")
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 二进制插件注册
|
||||
|
||||
使用插件加载器从文件系统加载二进制插件:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/loader"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化插件系统
|
||||
plugin.InitPluginSystem(taskManager, repoManager)
|
||||
|
||||
// 创建插件加载器
|
||||
pluginLoader := loader.NewSimplePluginLoader("./plugins")
|
||||
|
||||
// 加载所有插件
|
||||
plugins, err := pluginLoader.LoadAllPlugins()
|
||||
if err != nil {
|
||||
log.Printf("Failed to load plugins: %v", err)
|
||||
} else {
|
||||
// 注册加载的插件
|
||||
for _, p := range plugins {
|
||||
if err := plugin.RegisterPlugin(p); err != nil {
|
||||
log.Printf("Failed to register plugin %s: %v", p.Name(), err)
|
||||
} else {
|
||||
log.Printf("Successfully registered plugin: %s", p.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 插件配置
|
||||
|
||||
### 5.1 创建插件配置
|
||||
插件可以接收配置参数:
|
||||
|
||||
```go
|
||||
config := map[string]interface{}{
|
||||
"interval": 30, // 30秒间隔
|
||||
"enabled": true, // 启用插件
|
||||
"custom_param": "value",
|
||||
}
|
||||
|
||||
// 初始化插件时传入配置
|
||||
plugin.GetManager().InitializePlugin("my-plugin", config)
|
||||
```
|
||||
|
||||
### 5.2 在插件中使用配置
|
||||
|
||||
```go
|
||||
func (p *MyPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
ctx.LogInfo("MyPlugin initialized")
|
||||
|
||||
// 获取配置
|
||||
if interval, err := ctx.GetConfig("interval"); err == nil {
|
||||
if intVal, ok := interval.(int); ok {
|
||||
p.context.LogInfo("Interval set to: %d seconds", intVal)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 插件管理
|
||||
|
||||
### 6.1 启动插件
|
||||
|
||||
```go
|
||||
// 启动单个插件
|
||||
if err := plugin.GetManager().StartPlugin("my-plugin"); err != nil {
|
||||
log.Printf("Failed to start plugin: %v", err)
|
||||
}
|
||||
|
||||
// 启动所有插件
|
||||
plugins, _ := plugin.GetManager().GetAllPlugins()
|
||||
for _, name := range plugins {
|
||||
plugin.GetManager().StartPlugin(name)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 停止插件
|
||||
|
||||
```go
|
||||
// 停止单个插件
|
||||
if err := plugin.GetManager().StopPlugin("my-plugin"); err != nil {
|
||||
log.Printf("Failed to stop plugin: %v", err)
|
||||
}
|
||||
|
||||
// 停止所有插件
|
||||
plugins, _ := plugin.GetManager().GetAllPlugins()
|
||||
for _, name := range plugins {
|
||||
plugin.GetManager().StopPlugin(name)
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 检查插件状态
|
||||
|
||||
```go
|
||||
// 检查插件是否正在运行
|
||||
if plugin.GetManager().IsPluginRunning("my-plugin") {
|
||||
log.Println("Plugin is running")
|
||||
} else {
|
||||
log.Println("Plugin is not running")
|
||||
}
|
||||
|
||||
// 获取所有插件信息
|
||||
status := plugin.GetManager().GetAllPluginStatus()
|
||||
for name, info := range status {
|
||||
log.Printf("Plugin %s: %s", name, info.Status)
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 插件依赖管理
|
||||
|
||||
### 7.1 定义插件依赖
|
||||
|
||||
```go
|
||||
type MyPlugin struct {
|
||||
dependencies []string
|
||||
}
|
||||
|
||||
func (p *MyPlugin) Dependencies() []string {
|
||||
return []string{"dependency-plugin-1", "dependency-plugin-2"}
|
||||
}
|
||||
|
||||
func (p *MyPlugin) CheckDependencies() map[string]bool {
|
||||
result := make(map[string]bool)
|
||||
|
||||
// 检查依赖是否满足
|
||||
for _, dep := range p.dependencies {
|
||||
// 检查依赖插件是否存在且已启动
|
||||
result[dep] = plugin.GetManager().IsPluginRunning(dep)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 验证依赖
|
||||
|
||||
```go
|
||||
// 验证所有插件依赖
|
||||
if err := plugin.GetManager().ValidateDependencies(); err != nil {
|
||||
log.Printf("Dependency validation failed: %v", err)
|
||||
}
|
||||
|
||||
// 检查特定插件依赖
|
||||
ok, unresolved, err := plugin.GetManager().CheckPluginDependencies("my-plugin")
|
||||
if !ok {
|
||||
log.Printf("Unresolved dependencies: %v", unresolved)
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 实际使用示例
|
||||
|
||||
### 8.1 完整的插件使用示例
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/loader"
|
||||
"github.com/ctwj/urldb/task"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化日志
|
||||
utils.InitLogger(nil)
|
||||
|
||||
// 初始化数据库
|
||||
if err := db.InitDB(); err != nil {
|
||||
utils.Fatal("Failed to initialize database: %v", err)
|
||||
}
|
||||
|
||||
// 创建管理器
|
||||
repoManager := repo.NewRepositoryManager(db.DB)
|
||||
taskManager := task.NewTaskManager(repoManager)
|
||||
|
||||
// 初始化插件系统
|
||||
plugin.InitPluginSystem(taskManager, repoManager)
|
||||
|
||||
// 加载二进制插件
|
||||
loadBinaryPlugins()
|
||||
|
||||
// 注册源代码插件
|
||||
registerSourcePlugins()
|
||||
|
||||
// 等待运行
|
||||
log.Println("Plugin system ready. Running for 2 minutes...")
|
||||
time.Sleep(2 * time.Minute)
|
||||
|
||||
// 停止插件
|
||||
stopAllPlugins()
|
||||
}
|
||||
|
||||
func loadBinaryPlugins() {
|
||||
pluginLoader := loader.NewSimplePluginLoader("./plugins")
|
||||
|
||||
if plugins, err := pluginLoader.LoadAllPlugins(); err == nil {
|
||||
for _, p := range plugins {
|
||||
if err := plugin.RegisterPlugin(p); err != nil {
|
||||
log.Printf("Failed to register binary plugin %s: %v", p.Name(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerSourcePlugins() {
|
||||
// 注册源代码插件
|
||||
// myPlugin := my_plugin.NewMyPlugin()
|
||||
// plugin.RegisterPlugin(myPlugin)
|
||||
}
|
||||
|
||||
func stopAllPlugins() {
|
||||
plugins, _ := plugin.GetManager().GetAllPlugins()
|
||||
for _, name := range plugins {
|
||||
if err := plugin.GetManager().StopPlugin(name); err != nil {
|
||||
log.Printf("Failed to stop plugin %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 构建和部署
|
||||
|
||||
### 9.1 编译主程序
|
||||
|
||||
```bash
|
||||
cd examples/plugin_demo
|
||||
go build -o plugin_demo main.go
|
||||
./plugin_demo
|
||||
```
|
||||
|
||||
### 9.2 部署插件
|
||||
|
||||
1. 将编译好的 `.so` 插件文件复制到插件目录
|
||||
2. 确保主程序有读取插件文件的权限
|
||||
3. 根据需要配置插件参数
|
||||
|
||||
```bash
|
||||
# 创建插件目录
|
||||
mkdir -p plugins
|
||||
|
||||
# 复制插件
|
||||
cp my-plugin.so plugins/
|
||||
|
||||
# 运行主程序
|
||||
./plugin_demo
|
||||
```
|
||||
|
||||
## 10. 常见问题和解决方案
|
||||
|
||||
### 10.1 插件加载失败
|
||||
- 检查 `.so` 文件是否与主程序使用相同版本的 Go 编译
|
||||
- 确保插件依赖的库与主程序兼容
|
||||
- 检查插件文件权限
|
||||
|
||||
### 10.2 依赖关系问题
|
||||
- 确保插件依赖的其他插件已正确注册
|
||||
- 检查依赖关系循环
|
||||
|
||||
### 10.3 运行时错误
|
||||
- 确保插件实现符合接口要求
|
||||
- 检查插件初始化参数
|
||||
|
||||
## 11. 最佳实践
|
||||
|
||||
1. **插件命名**:使用有意义的插件名称
|
||||
2. **版本管理**:维护插件版本
|
||||
3. **错误处理**:在插件中添加适当的错误处理
|
||||
4. **日志记录**:使用插件上下文记录日志
|
||||
5. **资源清理**:确保在 `Cleanup` 方法中释放资源
|
||||
6. **安全考虑**:验证输入参数,限制资源使用
|
||||
7. **文档**:为插件提供使用文档
|
||||
36
examples/plugin_demo/binary_plugin1/go.mod
Normal file
36
examples/plugin_demo/binary_plugin1/go.mod
Normal file
@@ -0,0 +1,36 @@
|
||||
module github.com/ctwj/urldb/examples/plugin_demo/binary_plugin1
|
||||
|
||||
go 1.23.0
|
||||
|
||||
replace github.com/ctwj/urldb => ../../../..
|
||||
|
||||
require github.com/ctwj/urldb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.1 // 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/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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
golang.org/x/arch v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
82
examples/plugin_demo/binary_plugin1/go.sum
Normal file
82
examples/plugin_demo/binary_plugin1/go.sum
Normal file
@@ -0,0 +1,82 @@
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
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.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
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/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
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/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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
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/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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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=
|
||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
70
examples/plugin_demo/binary_plugin1/main.go
Normal file
70
examples/plugin_demo/binary_plugin1/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// Plugin1 示例插件1
|
||||
type Plugin1 struct{}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *Plugin1) Name() string {
|
||||
return "demo-plugin-1"
|
||||
}
|
||||
|
||||
// Version 返回插件版本
|
||||
func (p *Plugin1) Version() string {
|
||||
return "1.0.0"
|
||||
}
|
||||
|
||||
// Description 返回插件描述
|
||||
func (p *Plugin1) Description() string {
|
||||
return "这是一个简单的示例插件1"
|
||||
}
|
||||
|
||||
// Author 返回插件作者
|
||||
func (p *Plugin1) Author() string {
|
||||
return "Demo Author"
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *Plugin1) Initialize(ctx types.PluginContext) error {
|
||||
ctx.LogInfo("示例插件1初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动插件
|
||||
func (p *Plugin1) Start() error {
|
||||
fmt.Println("示例插件1启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止插件
|
||||
func (p *Plugin1) Stop() error {
|
||||
fmt.Println("示例插件1停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup 清理插件
|
||||
func (p *Plugin1) Cleanup() error {
|
||||
fmt.Println("示例插件1清理")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dependencies 返回插件依赖
|
||||
func (p *Plugin1) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckDependencies 检查插件依赖
|
||||
func (p *Plugin1) CheckDependencies() map[string]bool {
|
||||
return map[string]bool{}
|
||||
}
|
||||
|
||||
// 导出插件实例
|
||||
var Plugin = &Plugin1{}
|
||||
|
||||
func main() {
|
||||
// 编译为 .so 文件时,此函数不会被使用
|
||||
}
|
||||
36
examples/plugin_demo/binary_plugin2/go.mod
Normal file
36
examples/plugin_demo/binary_plugin2/go.mod
Normal file
@@ -0,0 +1,36 @@
|
||||
module github.com/ctwj/urldb/examples/plugin_demo/binary_plugin2
|
||||
|
||||
go 1.23.0
|
||||
|
||||
replace github.com/ctwj/urldb => ../../../..
|
||||
|
||||
require github.com/ctwj/urldb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.1 // 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/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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
golang.org/x/arch v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
82
examples/plugin_demo/binary_plugin2/go.sum
Normal file
82
examples/plugin_demo/binary_plugin2/go.sum
Normal file
@@ -0,0 +1,82 @@
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
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.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
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/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
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/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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
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/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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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=
|
||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
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=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
70
examples/plugin_demo/binary_plugin2/main.go
Normal file
70
examples/plugin_demo/binary_plugin2/main.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// Plugin2 示例插件2
|
||||
type Plugin2 struct{}
|
||||
|
||||
// Name 返回插件名称
|
||||
func (p *Plugin2) Name() string {
|
||||
return "demo-plugin-2"
|
||||
}
|
||||
|
||||
// Version 返回插件版本
|
||||
func (p *Plugin2) Version() string {
|
||||
return "1.0.0"
|
||||
}
|
||||
|
||||
// Description 返回插件描述
|
||||
func (p *Plugin2) Description() string {
|
||||
return "这是一个简单的示例插件2"
|
||||
}
|
||||
|
||||
// Author 返回插件作者
|
||||
func (p *Plugin2) Author() string {
|
||||
return "Demo Author"
|
||||
}
|
||||
|
||||
// Initialize 初始化插件
|
||||
func (p *Plugin2) Initialize(ctx types.PluginContext) error {
|
||||
ctx.LogInfo("示例插件2初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start 启动插件
|
||||
func (p *Plugin2) Start() error {
|
||||
fmt.Println("示例插件2启动")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop 停止插件
|
||||
func (p *Plugin2) Stop() error {
|
||||
fmt.Println("示例插件2停止")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup 清理插件
|
||||
func (p *Plugin2) Cleanup() error {
|
||||
fmt.Println("示例插件2清理")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dependencies 返回插件依赖
|
||||
func (p *Plugin2) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckDependencies 检查插件依赖
|
||||
func (p *Plugin2) CheckDependencies() map[string]bool {
|
||||
return map[string]bool{}
|
||||
}
|
||||
|
||||
// 导出插件实例
|
||||
var Plugin = &Plugin2{}
|
||||
|
||||
func main() {
|
||||
// 编译为 .so 文件时,此函数不会被使用
|
||||
}
|
||||
10
examples/plugin_demo/full_demo_plugin/go.mod
Normal file
10
examples/plugin_demo/full_demo_plugin/go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module github.com/ctwj/urldb/examples/plugin_demo/full_demo_plugin
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ctwj/urldb v0.0.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
replace github.com/ctwj/urldb => ../../..
|
||||
228
examples/plugin_demo/full_demo_plugin/plugin.go
Normal file
228
examples/plugin_demo/full_demo_plugin/plugin.go
Normal file
@@ -0,0 +1,228 @@
|
||||
package full_demo_plugin
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// FullDemoPlugin 是一个完整功能的演示插件
|
||||
type FullDemoPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
// NewFullDemoPlugin 创建完整演示插件
|
||||
func NewFullDemoPlugin() *FullDemoPlugin {
|
||||
return &FullDemoPlugin{
|
||||
name: "full-demo-plugin",
|
||||
version: "1.0.0",
|
||||
description: "A full-featured demo plugin demonstrating all plugin capabilities",
|
||||
author: "urlDB Team",
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the plugin name
|
||||
func (p *FullDemoPlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Version returns the plugin version
|
||||
func (p *FullDemoPlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
// Description returns the plugin description
|
||||
func (p *FullDemoPlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// Author returns the plugin author
|
||||
func (p *FullDemoPlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
// Initialize initializes the plugin
|
||||
func (p *FullDemoPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
p.context.LogInfo("Full Demo plugin initialized")
|
||||
|
||||
// 设置一些示例配置
|
||||
p.context.SetConfig("interval", 60)
|
||||
p.context.SetConfig("enabled", true)
|
||||
p.context.SetConfig("max_items", 100)
|
||||
|
||||
// 存储一些示例数据
|
||||
data := map[string]interface{}{
|
||||
"last_updated": time.Now().Format(time.RFC3339),
|
||||
"counter": 0,
|
||||
"status": "initialized",
|
||||
}
|
||||
p.context.SetData("demo_data", data, "demo_type")
|
||||
|
||||
// 获取并验证配置
|
||||
interval, err := p.context.GetConfig("interval")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to get interval config: %v", err)
|
||||
return err
|
||||
}
|
||||
p.context.LogInfo("Configured interval: %v", interval)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the plugin
|
||||
func (p *FullDemoPlugin) Start() error {
|
||||
p.context.LogInfo("Full Demo plugin started")
|
||||
|
||||
// 注册定时任务
|
||||
err := p.context.RegisterTask("demo-periodic-task", p.executePeriodicTask)
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to register periodic task: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 演示数据库访问
|
||||
p.demoDatabaseAccess()
|
||||
|
||||
// 演示数据存储功能
|
||||
p.demoDataStorage()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the plugin
|
||||
func (p *FullDemoPlugin) Stop() error {
|
||||
p.context.LogInfo("Full Demo plugin stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the plugin
|
||||
func (p *FullDemoPlugin) Cleanup() error {
|
||||
p.context.LogInfo("Full Demo plugin cleaned up")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dependencies returns the plugin dependencies
|
||||
func (p *FullDemoPlugin) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckDependencies checks the plugin dependencies
|
||||
func (p *FullDemoPlugin) CheckDependencies() map[string]bool {
|
||||
return make(map[string]bool)
|
||||
}
|
||||
|
||||
// executePeriodicTask 执行周期性任务
|
||||
func (p *FullDemoPlugin) executePeriodicTask() {
|
||||
p.context.LogInfo("Executing periodic task at %s", time.Now().Format(time.RFC3339))
|
||||
|
||||
// 从数据库获取数据
|
||||
data, err := p.context.GetData("demo_data", "demo_type")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to get demo data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Retrieved demo data: %v", data)
|
||||
|
||||
// 更新数据计数器
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
count, ok := dataMap["counter"].(float64) // json.Unmarshal converts numbers to float64
|
||||
if !ok {
|
||||
count = 0
|
||||
}
|
||||
count++
|
||||
|
||||
// 更新数据
|
||||
dataMap["counter"] = count
|
||||
dataMap["last_updated"] = time.Now().Format(time.RFC3339)
|
||||
dataMap["status"] = "running"
|
||||
|
||||
err = p.context.SetData("demo_data", dataMap, "demo_type")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to update demo data: %v", err)
|
||||
} else {
|
||||
p.context.LogInfo("Updated demo data, counter: %v", count)
|
||||
}
|
||||
}
|
||||
|
||||
// 演示配置访问
|
||||
enabled, err := p.context.GetConfig("enabled")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to get enabled config: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
p.context.LogInfo("Plugin enabled status: %v", enabled)
|
||||
}
|
||||
|
||||
// demoDatabaseAccess 演示数据库访问
|
||||
func (p *FullDemoPlugin) demoDatabaseAccess() {
|
||||
db := p.context.GetDB()
|
||||
if db == nil {
|
||||
p.context.LogError("Database connection not available")
|
||||
return
|
||||
}
|
||||
|
||||
// 将db转换为*gorm.DB
|
||||
gormDB, ok := db.(*gorm.DB)
|
||||
if !ok {
|
||||
p.context.LogError("Failed to cast database connection to *gorm.DB")
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试查询一些数据(如果存在的话)
|
||||
var count int64
|
||||
err := gormDB.Model(&entity.Resource{}).Count(&count).Error
|
||||
if err != nil {
|
||||
p.context.LogWarn("Failed to query resources: %v", err)
|
||||
} else {
|
||||
p.context.LogInfo("Database access demo: found %d resources", count)
|
||||
}
|
||||
}
|
||||
|
||||
// demoDataStorage 演示数据存储功能
|
||||
func (p *FullDemoPlugin) demoDataStorage() {
|
||||
// 存储一些复杂数据
|
||||
complexData := map[string]interface{}{
|
||||
"users": []map[string]interface{}{
|
||||
{"id": 1, "name": "Alice", "email": "alice@example.com"},
|
||||
{"id": 2, "name": "Bob", "email": "bob@example.com"},
|
||||
},
|
||||
"settings": map[string]interface{}{
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"notifications": true,
|
||||
},
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := p.context.SetData("complex_data", complexData, "user_settings")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to store complex data: %v", err)
|
||||
} else {
|
||||
p.context.LogInfo("Successfully stored complex data")
|
||||
}
|
||||
|
||||
// 读取复杂数据
|
||||
retrievedData, err := p.context.GetData("complex_data", "user_settings")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to retrieve complex data: %v", err)
|
||||
} else {
|
||||
p.context.LogInfo("Successfully retrieved complex data: %v", retrievedData)
|
||||
}
|
||||
|
||||
// 演示删除数据
|
||||
err = p.context.DeleteData("complex_data", "user_settings")
|
||||
if err != nil {
|
||||
p.context.LogError("Failed to delete data: %v", err)
|
||||
} else {
|
||||
p.context.LogInfo("Successfully deleted data")
|
||||
}
|
||||
}
|
||||
66
examples/plugin_demo/go.mod
Normal file
66
examples/plugin_demo/go.mod
Normal file
@@ -0,0 +1,66 @@
|
||||
module github.com/ctwj/urldb/examples/plugin_demo
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ctwj/urldb v0.0.0
|
||||
github.com/ctwj/urldb/examples/plugin_demo/full_demo_plugin v0.0.0
|
||||
github.com/ctwj/urldb/examples/plugin_demo/security_demo_plugin v0.0.0
|
||||
github.com/ctwj/urldb/examples/plugin_demo/uninstall_demo_plugin v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.1 // 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/go-resty/resty/v2 v2.16.5 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.5 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
golang.org/x/arch v0.19.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/postgres v1.6.0 // indirect
|
||||
gorm.io/gorm v1.30.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/ctwj/urldb => ../..
|
||||
github.com/ctwj/urldb/examples/plugin_demo/full_demo_plugin => ./full_demo_plugin
|
||||
github.com/ctwj/urldb/examples/plugin_demo/security_demo_plugin => ./security_demo_plugin
|
||||
github.com/ctwj/urldb/examples/plugin_demo/uninstall_demo_plugin => ./uninstall_demo_plugin
|
||||
)
|
||||
132
examples/plugin_demo/go.sum
Normal file
132
examples/plugin_demo/go.sum
Normal file
@@ -0,0 +1,132 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
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/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
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/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
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/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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
|
||||
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
142
examples/plugin_demo/main.go
Normal file
142
examples/plugin_demo/main.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/examples/plugin_demo/full_demo_plugin"
|
||||
"github.com/ctwj/urldb/examples/plugin_demo/security_demo_plugin"
|
||||
"github.com/ctwj/urldb/examples/plugin_demo/uninstall_demo_plugin"
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/loader"
|
||||
"github.com/ctwj/urldb/task"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize logger
|
||||
utils.InitLogger(nil)
|
||||
|
||||
// Initialize database
|
||||
if err := db.InitDB(); err != nil {
|
||||
utils.Fatal("Failed to initialize database: %v", err)
|
||||
}
|
||||
|
||||
// Create repository manager
|
||||
repoManager := repo.NewRepositoryManager(db.DB)
|
||||
|
||||
// Create task manager
|
||||
taskManager := task.NewTaskManager(repoManager)
|
||||
|
||||
// Initialize plugin system
|
||||
plugin.InitPluginSystem(taskManager, repoManager)
|
||||
|
||||
// Load binary plugins from filesystem
|
||||
// Create a simple plugin loader to load the binary plugins
|
||||
pluginLoader1 := loader.NewSimplePluginLoader("./binary_plugin1")
|
||||
pluginLoader2 := loader.NewSimplePluginLoader("./binary_plugin2")
|
||||
|
||||
// Load first binary plugin
|
||||
if plugins1, err := pluginLoader1.LoadAllPlugins(); err != nil {
|
||||
utils.Error("Failed to load binary plugin1: %v", err)
|
||||
// Continue execution even if plugin loading fails
|
||||
} else {
|
||||
// Register the loaded plugins
|
||||
for _, p := range plugins1 {
|
||||
if err := plugin.RegisterPlugin(p); err != nil {
|
||||
utils.Error("Failed to register binary plugin1: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load second binary plugin
|
||||
if plugins2, err := pluginLoader2.LoadAllPlugins(); err != nil {
|
||||
utils.Error("Failed to load binary plugin2: %v", err)
|
||||
// Continue execution even if plugin loading fails
|
||||
} else {
|
||||
// Register the loaded plugins
|
||||
for _, p := range plugins2 {
|
||||
if err := plugin.RegisterPlugin(p); err != nil {
|
||||
utils.Error("Failed to register binary plugin2: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register demo plugins from new structure
|
||||
fullDemoPlugin := full_demo_plugin.NewFullDemoPlugin()
|
||||
if err := plugin.RegisterPlugin(fullDemoPlugin); err != nil {
|
||||
utils.Error("Failed to register full demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
securityDemoPlugin := security_demo_plugin.NewSecurityDemoPlugin()
|
||||
if err := plugin.RegisterPlugin(securityDemoPlugin); err != nil {
|
||||
utils.Error("Failed to register security demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
uninstallDemoPlugin := uninstall_demo_plugin.NewUninstallDemoPlugin()
|
||||
if err := plugin.RegisterPlugin(uninstallDemoPlugin); err != nil {
|
||||
utils.Error("Failed to register uninstall demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize plugins
|
||||
config := map[string]interface{}{
|
||||
"interval": 30, // 30 seconds
|
||||
"enabled": true,
|
||||
}
|
||||
if err := plugin.GetManager().InitializePlugin("full-demo-plugin", config); err != nil {
|
||||
utils.Error("Failed to initialize full demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.GetManager().InitializePluginForHandler("security_demo"); err != nil {
|
||||
utils.Error("Failed to initialize security demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.GetManager().InitializePluginForHandler("uninstall-demo"); err != nil {
|
||||
utils.Error("Failed to initialize uninstall demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start plugins
|
||||
if err := plugin.GetManager().StartPlugin("full-demo-plugin"); err != nil {
|
||||
utils.Error("Failed to start full demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.GetManager().StartPlugin("security_demo"); err != nil {
|
||||
utils.Error("Failed to start security demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.GetManager().StartPlugin("uninstall-demo"); err != nil {
|
||||
utils.Error("Failed to start uninstall demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Keep the application running
|
||||
utils.Info("Plugin system test started. Running for 2 minutes...")
|
||||
time.Sleep(2 * time.Minute)
|
||||
|
||||
// Stop plugins
|
||||
if err := plugin.GetManager().StopPlugin("full-demo-plugin"); err != nil {
|
||||
utils.Error("Failed to stop full demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.GetManager().StopPlugin("security_demo"); err != nil {
|
||||
utils.Error("Failed to stop security demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := plugin.GetManager().StopPlugin("uninstall-demo"); err != nil {
|
||||
utils.Error("Failed to stop uninstall demo plugin: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("Plugin system test completed successfully.")
|
||||
}
|
||||
BIN
examples/plugin_demo/plugin_demo
Executable file
BIN
examples/plugin_demo/plugin_demo
Executable file
Binary file not shown.
9
examples/plugin_demo/security_demo_plugin/go.mod
Normal file
9
examples/plugin_demo/security_demo_plugin/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/ctwj/urldb/examples/plugin_demo/security_demo_plugin
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ctwj/urldb v0.0.0
|
||||
)
|
||||
|
||||
replace github.com/ctwj/urldb => ../../..
|
||||
115
examples/plugin_demo/security_demo_plugin/plugin.go
Normal file
115
examples/plugin_demo/security_demo_plugin/plugin.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package security_demo_plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/plugin/security"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// SecurityDemoPlugin is a demo plugin to demonstrate security features
|
||||
type SecurityDemoPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
config map[string]interface{}
|
||||
}
|
||||
|
||||
// NewSecurityDemoPlugin creates a new security demo plugin
|
||||
func NewSecurityDemoPlugin() *SecurityDemoPlugin {
|
||||
return &SecurityDemoPlugin{
|
||||
name: "security_demo",
|
||||
version: "1.0.0",
|
||||
description: "A demo plugin to demonstrate security features",
|
||||
author: "urlDB",
|
||||
config: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the plugin name
|
||||
func (p *SecurityDemoPlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Version returns the plugin version
|
||||
func (p *SecurityDemoPlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
// Description returns the plugin description
|
||||
func (p *SecurityDemoPlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// Author returns the plugin author
|
||||
func (p *SecurityDemoPlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
// Dependencies returns the plugin dependencies
|
||||
func (p *SecurityDemoPlugin) Dependencies() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// CheckDependencies checks the plugin dependencies
|
||||
func (p *SecurityDemoPlugin) CheckDependencies() map[string]bool {
|
||||
return make(map[string]bool)
|
||||
}
|
||||
|
||||
// Initialize initializes the plugin
|
||||
func (p *SecurityDemoPlugin) Initialize(ctx types.PluginContext) error {
|
||||
ctx.LogInfo("Initializing security demo plugin")
|
||||
|
||||
// Request additional permissions
|
||||
ctx.RequestPermission(string(security.PermissionConfigWrite), p.name)
|
||||
ctx.RequestPermission(string(security.PermissionDataWrite), p.name)
|
||||
|
||||
// Test permission
|
||||
hasPerm, err := ctx.CheckPermission(string(security.PermissionConfigRead))
|
||||
if err != nil {
|
||||
ctx.LogError("Error checking permission: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !hasPerm {
|
||||
ctx.LogWarn("Plugin does not have config read permission")
|
||||
return fmt.Errorf("plugin does not have required permissions")
|
||||
}
|
||||
|
||||
// Set some config
|
||||
ctx.SetConfig("initialized", true)
|
||||
ctx.SetConfig("timestamp", time.Now().Unix())
|
||||
|
||||
ctx.LogInfo("Security demo plugin initialized successfully")
|
||||
|
||||
// Register a demo task
|
||||
ctx.RegisterTask("security_demo_task", func() {
|
||||
ctx.LogInfo("Security demo task executed")
|
||||
// Try to access config
|
||||
if initialized, err := ctx.GetConfig("initialized"); err == nil {
|
||||
ctx.LogInfo("Plugin initialized: %v", initialized)
|
||||
}
|
||||
|
||||
// Try to write some data
|
||||
ctx.SetData("demo_key", "demo_value", "demo_type")
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the plugin
|
||||
func (p *SecurityDemoPlugin) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the plugin
|
||||
func (p *SecurityDemoPlugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the plugin
|
||||
func (p *SecurityDemoPlugin) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
9
examples/plugin_demo/uninstall_demo_plugin/go.mod
Normal file
9
examples/plugin_demo/uninstall_demo_plugin/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module github.com/ctwj/urldb/examples/plugin_demo/uninstall_demo_plugin
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/ctwj/urldb v0.0.0
|
||||
)
|
||||
|
||||
replace github.com/ctwj/urldb => ../../..
|
||||
127
examples/plugin_demo/uninstall_demo_plugin/plugin.go
Normal file
127
examples/plugin_demo/uninstall_demo_plugin/plugin.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package uninstall_demo_plugin
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
)
|
||||
|
||||
// UninstallDemoPlugin is a demo plugin to demonstrate uninstall functionality
|
||||
type UninstallDemoPlugin struct {
|
||||
name string
|
||||
version string
|
||||
description string
|
||||
author string
|
||||
dependencies []string
|
||||
context types.PluginContext
|
||||
}
|
||||
|
||||
// NewUninstallDemoPlugin creates a new uninstall demo plugin
|
||||
func NewUninstallDemoPlugin() *UninstallDemoPlugin {
|
||||
return &UninstallDemoPlugin{
|
||||
name: "uninstall-demo",
|
||||
version: "1.0.0",
|
||||
description: "A demo plugin to demonstrate uninstall functionality",
|
||||
author: "Plugin Developer",
|
||||
dependencies: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the plugin name
|
||||
func (p *UninstallDemoPlugin) Name() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Version returns the plugin version
|
||||
func (p *UninstallDemoPlugin) Version() string {
|
||||
return p.version
|
||||
}
|
||||
|
||||
// Description returns the plugin description
|
||||
func (p *UninstallDemoPlugin) Description() string {
|
||||
return p.description
|
||||
}
|
||||
|
||||
// Author returns the plugin author
|
||||
func (p *UninstallDemoPlugin) Author() string {
|
||||
return p.author
|
||||
}
|
||||
|
||||
// Dependencies returns the plugin dependencies
|
||||
func (p *UninstallDemoPlugin) Dependencies() []string {
|
||||
return p.dependencies
|
||||
}
|
||||
|
||||
// CheckDependencies checks the status of plugin dependencies
|
||||
func (p *UninstallDemoPlugin) CheckDependencies() map[string]bool {
|
||||
// For demo purposes, all dependencies are satisfied
|
||||
result := make(map[string]bool)
|
||||
for _, dep := range p.dependencies {
|
||||
result[dep] = true
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Initialize initializes the plugin
|
||||
func (p *UninstallDemoPlugin) Initialize(ctx types.PluginContext) error {
|
||||
p.context = ctx
|
||||
p.context.LogInfo("Initializing uninstall demo plugin")
|
||||
|
||||
// Set some demo configuration
|
||||
if err := p.context.SetConfig("demo_setting", "uninstall_demo_value"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set some demo data
|
||||
if err := p.context.SetData("demo_key", "uninstall_demo_data", "demo_type"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.context.LogInfo("Uninstall demo plugin initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts the plugin
|
||||
func (p *UninstallDemoPlugin) Start() error {
|
||||
p.context.LogInfo("Starting uninstall demo plugin")
|
||||
|
||||
// Simulate some work
|
||||
go func() {
|
||||
for i := 0; i < 5; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
p.context.LogInfo("Uninstall demo plugin working... %d", i+1)
|
||||
}
|
||||
p.context.LogInfo("Uninstall demo plugin work completed")
|
||||
}()
|
||||
|
||||
p.context.LogInfo("Uninstall demo plugin started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the plugin
|
||||
func (p *UninstallDemoPlugin) Stop() error {
|
||||
p.context.LogInfo("Stopping uninstall demo plugin")
|
||||
|
||||
// Perform any necessary cleanup before stopping
|
||||
p.context.LogInfo("Uninstall demo plugin stopped successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup performs final cleanup when uninstalling the plugin
|
||||
func (p *UninstallDemoPlugin) Cleanup() error {
|
||||
p.context.LogInfo("Cleaning up uninstall demo plugin")
|
||||
|
||||
// Perform any final cleanup operations
|
||||
// This might include:
|
||||
// - Removing temporary files
|
||||
// - Cleaning up external resources
|
||||
// - Notifying external services
|
||||
|
||||
p.context.LogInfo("Uninstall demo plugin cleaned up successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetContext returns the plugin context (for testing purposes)
|
||||
func (p *UninstallDemoPlugin) GetContext() types.PluginContext {
|
||||
return p.context
|
||||
}
|
||||
27
go.mod
27
go.mod
@@ -12,15 +12,31 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/meilisearch/meilisearch-go v0.33.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
golang.org/x/crypto v0.40.0
|
||||
github.com/silenceper/wechat/v2 v2.1.10
|
||||
golang.org/x/crypto v0.41.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -41,7 +57,6 @@ require (
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -52,10 +67,10 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
golang.org/x/arch v0.19.0 // indirect
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/net v0.43.0
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
164
go.sum
164
go.sum
@@ -1,10 +1,24 @@
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.30.0 h1:uA3uhDbCxfO9+DI/DuGeAMr9qI+noVWwGPNTFuKID5M=
|
||||
github.com/alicebob/miniredis/v2 v2.30.0/go.mod h1:84TWKZlxYkfgMucPBf5SOQBYJceZeQRFIaQgNMiCX6Q=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||
github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
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/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
@@ -12,6 +26,12 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
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.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
|
||||
@@ -34,8 +54,11 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
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/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
|
||||
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
@@ -45,11 +68,27 @@ github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXe
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
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/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -66,6 +105,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
@@ -79,6 +120,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
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=
|
||||
@@ -94,30 +137,67 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
|
||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/silenceper/wechat/v2 v2.1.10 h1:jMg0//CZBIuogEvuXgxJQuJ47SsPPAqFrrbOtro2pko=
|
||||
github.com/silenceper/wechat/v2 v2.1.10/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
||||
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
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 v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
@@ -126,41 +206,99 @@ 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=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ=
|
||||
github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
golang.org/x/arch v0.19.0 h1:LmbDQUodHThXE+htjrnmVD73M//D9GTH6wFZjyDkjyU=
|
||||
golang.org/x/arch v0.19.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
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/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
|
||||
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
100
handlers/api_access_log_handler.go
Normal file
100
handlers/api_access_log_handler.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetAPIAccessLogs 获取API访问日志
|
||||
func GetAPIAccessLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
endpoint := c.Query("endpoint")
|
||||
ip := c.Query("ip")
|
||||
|
||||
var startDate, endDate *time.Time
|
||||
|
||||
if startDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil {
|
||||
startDate = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
if endDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
|
||||
// 设置为当天结束时间
|
||||
endOfDay := parsed.Add(24*time.Hour - time.Second)
|
||||
endDate = &endOfDay
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
logs, total, err := repoManager.APIAccessLogRepository.FindWithFilters(page, pageSize, startDate, endDate, endpoint, ip)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取API访问日志失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToAPIAccessLogResponseList(logs)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": response,
|
||||
"total": int(total),
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAPIAccessLogSummary 获取API访问日志汇总
|
||||
func GetAPIAccessLogSummary(c *gin.Context) {
|
||||
summary, err := repoManager.APIAccessLogRepository.GetSummary()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取API访问日志汇总失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToAPIAccessLogSummaryResponse(summary)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// GetAPIAccessLogStats 获取API访问日志统计
|
||||
func GetAPIAccessLogStats(c *gin.Context) {
|
||||
stats, err := repoManager.APIAccessLogRepository.GetStatsByEndpoint()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取API访问日志统计失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToAPIAccessLogStatsResponseList(stats)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// ClearAPIAccessLogs 清理API访问日志
|
||||
func ClearAPIAccessLogs(c *gin.Context) {
|
||||
daysStr := c.Query("days")
|
||||
if daysStr == "" {
|
||||
ErrorResponse(c, "请提供要清理的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil || days < 1 {
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.APIAccessLogRepository.ClearOldLogs(days)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "清理API访问日志失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "API访问日志清理成功"})
|
||||
}
|
||||
@@ -440,3 +440,80 @@ func (h *FileHandler) calculateFileHash(filePath string) (string, error) {
|
||||
}
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// UploadWechatVerifyFile 上传微信公众号验证文件(TXT文件)
|
||||
// 无需认证,仅支持TXT文件,不记录数据库,直接保存到uploads目录
|
||||
func (h *FileHandler) UploadWechatVerifyFile(c *gin.Context) {
|
||||
// 获取上传的文件
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
ErrorResponse(c, "未提供文件", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件扩展名必须是.txt
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
if ext != ".txt" {
|
||||
ErrorResponse(c, "仅支持TXT文件", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小(限制1MB)
|
||||
if file.Size > 1*1024*1024 {
|
||||
ErrorResponse(c, "文件大小不能超过1MB", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成文件名(使用原始文件名,但确保是安全的)
|
||||
originalName := filepath.Base(file.Filename)
|
||||
safeFileName := h.makeSafeFileName(originalName)
|
||||
|
||||
// 确保uploads目录存在
|
||||
uploadsDir := "./uploads"
|
||||
if err := os.MkdirAll(uploadsDir, 0755); err != nil {
|
||||
ErrorResponse(c, "创建上传目录失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建完整文件路径
|
||||
filePath := filepath.Join(uploadsDir, safeFileName)
|
||||
|
||||
// 保存文件
|
||||
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||
ErrorResponse(c, "保存文件失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置文件权限
|
||||
if err := os.Chmod(filePath, 0644); err != nil {
|
||||
utils.Warn("设置文件权限失败: %v", err)
|
||||
}
|
||||
|
||||
// 返回成功响应
|
||||
accessURL := fmt.Sprintf("/%s", safeFileName)
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "验证文件上传成功",
|
||||
"file_name": safeFileName,
|
||||
"access_url": accessURL,
|
||||
}
|
||||
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
// makeSafeFileName 生成安全的文件名,移除危险字符
|
||||
func (h *FileHandler) makeSafeFileName(filename string) string {
|
||||
// 移除路径分隔符和特殊字符
|
||||
safeName := strings.ReplaceAll(filename, "/", "_")
|
||||
safeName = strings.ReplaceAll(safeName, "\\", "_")
|
||||
safeName = strings.ReplaceAll(safeName, "..", "_")
|
||||
|
||||
// 限制文件名长度
|
||||
if len(safeName) > 100 {
|
||||
ext := filepath.Ext(safeName)
|
||||
name := safeName[:100-len(ext)]
|
||||
safeName = name + ext
|
||||
}
|
||||
|
||||
return safeName
|
||||
}
|
||||
|
||||
188
handlers/log_handler.go
Normal file
188
handlers/log_handler.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetSystemLogs 获取系统日志
|
||||
func GetSystemLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
|
||||
level := c.Query("level")
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
search := c.Query("search")
|
||||
|
||||
var startDate, endDate *time.Time
|
||||
|
||||
if startDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", startDateStr); err == nil {
|
||||
startDate = &parsed
|
||||
}
|
||||
}
|
||||
|
||||
if endDateStr != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", endDateStr); err == nil {
|
||||
// 设置为当天结束时间
|
||||
endOfDay := parsed.Add(24*time.Hour - time.Second)
|
||||
endDate = &endOfDay
|
||||
}
|
||||
}
|
||||
|
||||
// 使用日志查看器获取日志
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
|
||||
// 获取日志文件列表
|
||||
logFiles, err := logViewer.GetLogFiles()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志文件失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果指定了日期范围,只选择对应日期的日志文件
|
||||
if startDate != nil || endDate != nil {
|
||||
var filteredFiles []string
|
||||
for _, file := range logFiles {
|
||||
fileInfo, err := utils.GetFileInfo(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
shouldInclude := true
|
||||
if startDate != nil {
|
||||
if fileInfo.ModTime().Before(*startDate) {
|
||||
shouldInclude = false
|
||||
}
|
||||
}
|
||||
if endDate != nil {
|
||||
if fileInfo.ModTime().After(*endDate) {
|
||||
shouldInclude = false
|
||||
}
|
||||
}
|
||||
|
||||
if shouldInclude {
|
||||
filteredFiles = append(filteredFiles, file)
|
||||
}
|
||||
}
|
||||
logFiles = filteredFiles
|
||||
}
|
||||
|
||||
// 限制读取的文件数量以提高性能
|
||||
if len(logFiles) > 10 {
|
||||
logFiles = logFiles[:10] // 只处理最近的10个文件
|
||||
}
|
||||
|
||||
var allLogs []utils.LogEntry
|
||||
for _, file := range logFiles {
|
||||
// 读取日志文件
|
||||
fileLogs, err := logViewer.ParseLogEntriesFromFile(file, level, search)
|
||||
if err != nil {
|
||||
utils.Error("解析日志文件失败 %s: %v", file, err)
|
||||
continue
|
||||
}
|
||||
allLogs = append(allLogs, fileLogs...)
|
||||
}
|
||||
|
||||
// 按时间排序(最新的在前)
|
||||
utils.SortLogEntriesByTime(allLogs, false)
|
||||
|
||||
// 应用分页
|
||||
start := (page - 1) * pageSize
|
||||
end := start + pageSize
|
||||
if start > len(allLogs) {
|
||||
start = len(allLogs)
|
||||
}
|
||||
if end > len(allLogs) {
|
||||
end = len(allLogs)
|
||||
}
|
||||
|
||||
pagedLogs := allLogs[start:end]
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": pagedLogs,
|
||||
"total": len(allLogs),
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSystemLogFiles 获取系统日志文件列表
|
||||
func GetSystemLogFiles(c *gin.Context) {
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
files, err := logViewer.GetLogFiles()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志文件列表失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取每个文件的详细信息
|
||||
var fileInfos []gin.H
|
||||
for _, file := range files {
|
||||
info, err := utils.GetFileInfo(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
fileInfos = append(fileInfos, gin.H{
|
||||
"name": info.Name(),
|
||||
"size": info.Size(),
|
||||
"mod_time": info.ModTime(),
|
||||
"path": file,
|
||||
})
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": fileInfos,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSystemLogSummary 获取系统日志统计摘要
|
||||
func GetSystemLogSummary(c *gin.Context) {
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
files, err := logViewer.GetLogFiles()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志文件列表失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
stats, err := logViewer.GetLogStats(files)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取日志统计信息失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"summary": stats,
|
||||
"files_count": len(files),
|
||||
})
|
||||
}
|
||||
|
||||
// ClearSystemLogs 清理系统日志
|
||||
func ClearSystemLogs(c *gin.Context) {
|
||||
daysStr := c.Query("days")
|
||||
if daysStr == "" {
|
||||
ErrorResponse(c, "请提供要清理的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
days, err := strconv.Atoi(daysStr)
|
||||
if err != nil || days < 1 {
|
||||
ErrorResponse(c, "无效的天数参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logViewer := utils.NewLogViewer("logs")
|
||||
err = logViewer.CleanOldLogs(days)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "清理系统日志失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "系统日志清理成功"})
|
||||
}
|
||||
399
handlers/plugin_handler.go
Normal file
399
handlers/plugin_handler.go
Normal file
@@ -0,0 +1,399 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/types"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PluginHandler 插件管理处理器
|
||||
type PluginHandler struct{}
|
||||
|
||||
// NewPluginHandler 创建插件管理处理器
|
||||
func NewPluginHandler() *PluginHandler {
|
||||
return &PluginHandler{}
|
||||
}
|
||||
|
||||
// GetPlugins 获取所有插件信息
|
||||
func (ph *PluginHandler) GetPlugins(c *gin.Context) {
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
plugins := manager.ListPlugins()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"plugins": plugins,
|
||||
"count": len(plugins),
|
||||
})
|
||||
}
|
||||
|
||||
// GetPlugin 获取指定插件信息
|
||||
func (ph *PluginHandler) GetPlugin(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
pluginInfo, err := manager.GetPluginInfo(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, pluginInfo)
|
||||
}
|
||||
|
||||
// InstallPlugin 安装插件
|
||||
func (ph *PluginHandler) InstallPlugin(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试从上传的文件安装
|
||||
file, err := c.FormFile("file")
|
||||
if err == nil {
|
||||
// 保存上传的文件到临时位置
|
||||
tempPath := filepath.Join(os.TempDir(), file.Filename)
|
||||
if err := c.SaveUploadedFile(file, tempPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save uploaded file: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 安装插件
|
||||
if err := manager.InstallPluginFromFile(tempPath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to install plugin: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
defer os.Remove(tempPath)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin installed successfully",
|
||||
"name": pluginName,
|
||||
"file": file.Filename,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有上传文件,尝试从请求体中获取文件路径参数
|
||||
filepath := c.PostForm("filepath")
|
||||
if filepath == "" {
|
||||
// 如果仍然没有文件路径参数,返回错误
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Either upload a file or provide a file path"})
|
||||
return
|
||||
}
|
||||
|
||||
// 安装插件
|
||||
if err := manager.InstallPluginFromFile(filepath); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to install plugin: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin installed successfully",
|
||||
"name": pluginName,
|
||||
"path": filepath,
|
||||
})
|
||||
}
|
||||
|
||||
// UninstallPlugin 卸载插件
|
||||
func (ph *PluginHandler) UninstallPlugin(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
force := false
|
||||
if c.Query("force") == "true" {
|
||||
force = true
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.UninstallPlugin(pluginName, force); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin uninstalled successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// InitializePlugin 初始化插件
|
||||
func (ph *PluginHandler) InitializePlugin(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.InitializePluginForHandler(pluginName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin initialized successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// StartPlugin 启动插件
|
||||
func (ph *PluginHandler) StartPlugin(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.StartPlugin(pluginName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin started successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// StopPlugin 停止插件
|
||||
func (ph *PluginHandler) StopPlugin(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := manager.StopPlugin(pluginName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin stopped successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetPluginConfig 获取插件配置
|
||||
func (ph *PluginHandler) GetPluginConfig(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
instance, err := manager.GetPluginInstance(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果插件支持配置接口,获取配置
|
||||
config := make(map[string]interface{})
|
||||
if configurablePlugin, ok := instance.Plugin.(interface{ GetConfig() map[string]interface{} }); ok {
|
||||
config = configurablePlugin.GetConfig()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"plugin_name": pluginName,
|
||||
"config": config,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdatePluginConfig 更新插件配置
|
||||
func (ph *PluginHandler) UpdatePluginConfig(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
var config map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid configuration format: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件实例验证插件是否存在
|
||||
instance, err := manager.GetPluginInstance(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查插件是否支持配置更新
|
||||
configurablePlugin, ok := instance.Plugin.(interface{ UpdateConfig(map[string]interface{}) error })
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin does not support configuration updates"})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果插件正在运行,先停止
|
||||
instanceInfo, _ := manager.GetPluginInstance(pluginName)
|
||||
if instanceInfo.Status == types.StatusRunning {
|
||||
if err := manager.StopPlugin(pluginName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to stop plugin for configuration update: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
if err := configurablePlugin.UpdateConfig(config); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update configuration: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 如果插件之前是运行状态,重新启动
|
||||
if instanceInfo.Status == types.StatusRunning {
|
||||
if err := manager.StartPlugin(pluginName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to restart plugin after configuration update: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Plugin configuration updated successfully",
|
||||
"config": config,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPluginDependencies 获取插件依赖信息
|
||||
func (ph *PluginHandler) GetPluginDependencies(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
pluginInfo, err := manager.GetPluginInfo(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件实例
|
||||
instance, err := manager.GetPluginInstance(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取依赖项列表(如果插件支持依赖接口)
|
||||
dependencies := make([]string, 0)
|
||||
dependents := make([]string, 0)
|
||||
|
||||
if dependencyPlugin, ok := instance.Plugin.(interface{ Dependencies() []string }); ok {
|
||||
dependencies = dependencyPlugin.Dependencies()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"plugin_info": pluginInfo,
|
||||
"dependencies": dependencies,
|
||||
"dependents": dependents,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPluginLoadOrder 获取插件加载顺序
|
||||
func (ph *PluginHandler) GetPluginLoadOrder(c *gin.Context) {
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 简化版管理器直接返回所有插件名称
|
||||
plugins := manager.ListPlugins()
|
||||
loadOrder := make([]string, len(plugins))
|
||||
for i, plugin := range plugins {
|
||||
loadOrder[i] = plugin.Name
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"load_order": loadOrder,
|
||||
"count": len(loadOrder),
|
||||
})
|
||||
}
|
||||
|
||||
// ValidatePluginDependencies 验证插件依赖
|
||||
func (ph *PluginHandler) ValidatePluginDependencies(c *gin.Context) {
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有插件注册
|
||||
plugins := manager.ListPlugins()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": len(plugins) > 0, // 简单验证:如果有插件则认为有效
|
||||
"count": len(plugins),
|
||||
"plugins": plugins,
|
||||
"message": fmt.Sprintf("Found %d plugins", len(plugins)),
|
||||
})
|
||||
}
|
||||
248
handlers/plugin_monitor_handler.go
Normal file
248
handlers/plugin_monitor_handler.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
"github.com/ctwj/urldb/plugin/monitor"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PluginMonitorHandler 插件监控处理器
|
||||
type PluginMonitorHandler struct {
|
||||
monitor *monitor.PluginMonitor
|
||||
checker *monitor.PluginHealthChecker
|
||||
}
|
||||
|
||||
// NewPluginMonitorHandler 创建插件监控处理器
|
||||
func NewPluginMonitorHandler() *PluginMonitorHandler {
|
||||
pluginMonitor := plugin.GetPluginMonitor()
|
||||
healthChecker := monitor.NewPluginHealthChecker(pluginMonitor)
|
||||
|
||||
return &PluginMonitorHandler{
|
||||
monitor: pluginMonitor,
|
||||
checker: healthChecker,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPluginHealth 获取插件健康状态
|
||||
func (pmh *PluginMonitorHandler) GetPluginHealth(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件管理器
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件
|
||||
pluginInstance, err := manager.GetPlugin(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Plugin not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 执行健康检查
|
||||
result := pmh.checker.Check(c.Request.Context(), pluginInstance)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetAllPluginsHealth 获取所有插件健康状态
|
||||
func (pmh *PluginMonitorHandler) GetAllPluginsHealth(c *gin.Context) {
|
||||
// 获取插件管理器
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有插件
|
||||
plugins := manager.GetEnabledPlugins()
|
||||
|
||||
// 执行批量健康检查
|
||||
results := pmh.checker.BatchCheck(c.Request.Context(), plugins)
|
||||
|
||||
// 生成报告
|
||||
report := pmh.checker.GenerateReport(results)
|
||||
|
||||
c.JSON(http.StatusOK, report)
|
||||
}
|
||||
|
||||
// GetPluginActivities 获取插件活动记录
|
||||
func (pmh *PluginMonitorHandler) GetPluginActivities(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
limit := 100 // 默认限制100条记录
|
||||
if limitParam := c.Query("limit"); limitParam != "" {
|
||||
// 这里可以解析limit参数
|
||||
}
|
||||
|
||||
activities := pmh.monitor.GetActivities(pluginName, limit)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"plugin_name": pluginName,
|
||||
"activities": activities,
|
||||
"count": len(activities),
|
||||
})
|
||||
}
|
||||
|
||||
// GetPluginMetrics 获取插件指标
|
||||
func (pmh *PluginMonitorHandler) GetPluginMetrics(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
metrics := pmh.monitor.GetMonitorStats()
|
||||
|
||||
c.JSON(http.StatusOK, metrics)
|
||||
}
|
||||
|
||||
// GetAlertRules 获取告警规则
|
||||
func (pmh *PluginMonitorHandler) GetAlertRules(c *gin.Context) {
|
||||
rules := pmh.monitor.GetAlertRules()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"rules": rules,
|
||||
"count": len(rules),
|
||||
})
|
||||
}
|
||||
|
||||
// CreateAlertRule 创建告警规则
|
||||
func (pmh *PluginMonitorHandler) CreateAlertRule(c *gin.Context) {
|
||||
var rule monitor.AlertRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := pmh.monitor.SetAlertRule(rule); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Alert rule created successfully",
|
||||
"rule": rule,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAlertRule 删除告警规则
|
||||
func (pmh *PluginMonitorHandler) DeleteAlertRule(c *gin.Context) {
|
||||
ruleName := c.Param("name")
|
||||
if ruleName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Rule name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
pmh.monitor.RemoveAlertRule(ruleName)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Alert rule deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetPluginMonitorStats 获取插件监控统计信息
|
||||
func (pmh *PluginMonitorHandler) GetPluginMonitorStats(c *gin.Context) {
|
||||
stats := pmh.monitor.GetMonitorStats()
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// StreamAlerts 流式获取告警信息
|
||||
func (pmh *PluginMonitorHandler) StreamAlerts(c *gin.Context) {
|
||||
// 设置SSE头
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// 获取告警通道
|
||||
alerts := pmh.monitor.GetAlerts()
|
||||
|
||||
// 客户端断开连接时的处理
|
||||
clientGone := c.Request.Context().Done()
|
||||
|
||||
// 发送初始连接消息
|
||||
c.Stream(func(w io.Writer) bool {
|
||||
select {
|
||||
case alert := <-alerts:
|
||||
// 发送告警信息
|
||||
c.SSEvent("alert", alert)
|
||||
return true
|
||||
case <-clientGone:
|
||||
// 客户端断开连接
|
||||
return false
|
||||
case <-time.After(30 * time.Second):
|
||||
// 发送心跳消息保持连接
|
||||
c.SSEvent("ping", time.Now().Unix())
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// GetPluginHealthHistory 获取插件健康历史
|
||||
func (pmh *PluginMonitorHandler) GetPluginHealthHistory(c *gin.Context) {
|
||||
pluginName := c.Param("name")
|
||||
if pluginName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Plugin name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件管理器
|
||||
manager := plugin.GetManager()
|
||||
if manager == nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Plugin manager not initialized"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取插件信息
|
||||
pluginInfo, err := manager.GetPluginInfo(pluginName)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Plugin not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取活动记录作为健康历史的替代
|
||||
activities := pmh.monitor.GetActivities(pluginName, 50)
|
||||
|
||||
// 构建健康历史数据
|
||||
history := make([]map[string]interface{}, 0)
|
||||
for _, activity := range activities {
|
||||
if activity.Error != nil {
|
||||
history = append(history, map[string]interface{}{
|
||||
"timestamp": activity.Timestamp,
|
||||
"status": "error",
|
||||
"message": activity.Error.Error(),
|
||||
"operation": activity.Operation,
|
||||
"duration": activity.ExecutionTime,
|
||||
})
|
||||
} else {
|
||||
history = append(history, map[string]interface{}{
|
||||
"timestamp": activity.Timestamp,
|
||||
"status": "success",
|
||||
"operation": activity.Operation,
|
||||
"duration": activity.ExecutionTime,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"plugin": pluginInfo,
|
||||
"history": history,
|
||||
"count": len(history),
|
||||
})
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
@@ -69,6 +70,53 @@ func (h *PublicAPIHandler) filterForbiddenWords(resources []entity.Resource) ([]
|
||||
return filteredResources, uniqueForbiddenWords
|
||||
}
|
||||
|
||||
// logAPIAccess 记录API访问日志
|
||||
func (h *PublicAPIHandler) logAPIAccess(c *gin.Context, startTime time.Time, processCount int, responseData interface{}, errorMessage string) {
|
||||
endpoint := c.Request.URL.Path
|
||||
method := c.Request.Method
|
||||
ip := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
// 计算处理时间
|
||||
processingTime := time.Since(startTime).Milliseconds()
|
||||
|
||||
// 获取查询参数
|
||||
var requestParams interface{}
|
||||
if method == "GET" {
|
||||
requestParams = c.Request.URL.Query()
|
||||
} else {
|
||||
// 对于POST请求,尝试从上下文中获取请求体(如果之前已解析)
|
||||
if req, exists := c.Get("request_body"); exists {
|
||||
requestParams = req
|
||||
}
|
||||
}
|
||||
|
||||
// 异步记录日志,避免影响API响应时间
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
utils.Error("记录API访问日志时发生panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
err := repoManager.APIAccessLogRepository.RecordAccess(
|
||||
ip,
|
||||
userAgent,
|
||||
endpoint,
|
||||
method,
|
||||
requestParams,
|
||||
c.Writer.Status(),
|
||||
responseData,
|
||||
processCount,
|
||||
errorMessage,
|
||||
processingTime,
|
||||
)
|
||||
if err != nil {
|
||||
utils.Error("记录API访问日志失败: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// AddBatchResources godoc
|
||||
// @Summary 批量添加资源
|
||||
// @Description 通过公开API批量添加多个资源到待处理列表
|
||||
@@ -83,17 +131,28 @@ func (h *PublicAPIHandler) filterForbiddenWords(resources []entity.Resource) ([]
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/resources/batch-add [post]
|
||||
func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
var req dto.BatchReadyResourceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logAPIAccess(c, startTime, 0, nil, "请求参数错误: "+err.Error())
|
||||
ErrorResponse(c, "请求参数错误: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 存储请求体用于日志记录
|
||||
c.Set("request_body", req)
|
||||
|
||||
if len(req.Resources) == 0 {
|
||||
ErrorResponse(c, "资源列表不能为空", 400)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录API访问安全日志
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
utils.Info("PublicAPI.AddBatchResources - API访问 - IP: %s, UserAgent: %s, 资源数量: %d", clientIP, userAgent, len(req.Resources))
|
||||
|
||||
// 收集所有待提交的URL,去重
|
||||
urlSet := make(map[string]struct{})
|
||||
for _, resource := range req.Resources {
|
||||
@@ -125,6 +184,7 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
// 生成 key(每组同一个 key)
|
||||
key, err := repoManager.ReadyResourceRepository.GenerateUniqueKey()
|
||||
if err != nil {
|
||||
h.logAPIAccess(c, startTime, len(createdResources), nil, "生成资源组标识失败: "+err.Error())
|
||||
ErrorResponse(c, "生成资源组标识失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -156,10 +216,12 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
responseData := gin.H{
|
||||
"created_count": len(createdResources),
|
||||
"created_ids": createdResources,
|
||||
})
|
||||
}
|
||||
h.logAPIAccess(c, startTime, len(createdResources), responseData, "")
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
// SearchResources godoc
|
||||
@@ -179,7 +241,11 @@ func (h *PublicAPIHandler) AddBatchResources(c *gin.Context) {
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/resources/search [get]
|
||||
func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
// 获取查询参数
|
||||
startTime := time.Now()
|
||||
|
||||
// 记录API访问安全日志
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
keyword := c.Query("keyword")
|
||||
tag := c.Query("tag")
|
||||
category := c.Query("category")
|
||||
@@ -187,6 +253,9 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "20")
|
||||
|
||||
utils.Info("PublicAPI.SearchResources - API访问 - IP: %s, UserAgent: %s, Keyword: %s, Tag: %s, Category: %s, PanID: %s",
|
||||
clientIP, userAgent, keyword, tag, category, panID)
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
@@ -236,6 +305,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
FileSize: doc.FileSize,
|
||||
Key: doc.Key,
|
||||
PanID: doc.PanID,
|
||||
Cover: doc.Cover,
|
||||
CreatedAt: doc.CreatedAt,
|
||||
UpdatedAt: doc.UpdatedAt,
|
||||
}
|
||||
@@ -276,6 +346,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
// 执行数据库搜索
|
||||
resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
if err != nil {
|
||||
h.logAPIAccess(c, startTime, 0, nil, "搜索失败: "+err.Error())
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -304,6 +375,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
"view_count": processedResource.ViewCount,
|
||||
"created_at": processedResource.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
"updated_at": processedResource.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
"cover": processedResource.Cover, // 添加封面字段
|
||||
}
|
||||
|
||||
// 添加违禁词标记
|
||||
@@ -320,6 +392,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
"limit": pageSize,
|
||||
}
|
||||
|
||||
h.logAPIAccess(c, startTime, len(resourceResponses), responseData, "")
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
@@ -337,9 +410,16 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/public/hot-dramas [get]
|
||||
func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 记录API访问安全日志
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "20")
|
||||
|
||||
utils.Info("PublicAPI.GetHotDramas - API访问 - IP: %s, UserAgent: %s", clientIP, userAgent)
|
||||
|
||||
page, err := strconv.Atoi(pageStr)
|
||||
if err != nil || page < 1 {
|
||||
page = 1
|
||||
@@ -353,6 +433,7 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
// 获取热门剧
|
||||
hotDramas, total, err := repoManager.HotDramaRepository.FindAll(page, pageSize)
|
||||
if err != nil {
|
||||
h.logAPIAccess(c, startTime, 0, nil, "获取热门剧失败: "+err.Error())
|
||||
ErrorResponse(c, "获取热门剧失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
@@ -376,10 +457,12 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
responseData := gin.H{
|
||||
"hot_dramas": hotDramaResponses,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
h.logAPIAccess(c, startTime, len(hotDramaResponses), responseData, "")
|
||||
SuccessResponse(c, responseData)
|
||||
}
|
||||
|
||||
@@ -188,6 +188,7 @@ func GetResources(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
resourceResponse["tags"] = tagResponses
|
||||
resourceResponse["cover"] = originalResource.Cover
|
||||
|
||||
resourceResponses = append(resourceResponses, resourceResponse)
|
||||
}
|
||||
@@ -394,12 +395,29 @@ func DeleteResource(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 先从数据库中删除资源
|
||||
err = repoManager.ResourceRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果启用了Meilisearch,尝试从Meilisearch中删除对应数据
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
go func() {
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
if err := service.DeleteDocument(uint(id)); err != nil {
|
||||
utils.Error("从Meilisearch删除资源失败 (ID: %d): %v", uint(id), err)
|
||||
} else {
|
||||
utils.Info("成功从Meilisearch删除资源 (ID: %d)", uint(id))
|
||||
}
|
||||
} else {
|
||||
utils.Error("无法获取Meilisearch服务进行资源删除 (ID: %d)", uint(id))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "资源删除成功"})
|
||||
}
|
||||
|
||||
@@ -512,12 +530,39 @@ func BatchDeleteResources(c *gin.Context) {
|
||||
ErrorResponse(c, "参数错误", 400)
|
||||
return
|
||||
}
|
||||
|
||||
count := 0
|
||||
var deletedIDs []uint
|
||||
|
||||
// 先删除数据库中的资源
|
||||
for _, id := range req.IDs {
|
||||
if err := repoManager.ResourceRepository.Delete(id); err == nil {
|
||||
count++
|
||||
deletedIDs = append(deletedIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果启用了Meilisearch,异步删除对应的搜索数据
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() && len(deletedIDs) > 0 {
|
||||
go func() {
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
deletedCount := 0
|
||||
for _, id := range deletedIDs {
|
||||
if err := service.DeleteDocument(id); err != nil {
|
||||
utils.Error("从Meilisearch批量删除资源失败 (ID: %d): %v", id, err)
|
||||
} else {
|
||||
deletedCount++
|
||||
utils.Info("成功从Meilisearch批量删除资源 (ID: %d)", id)
|
||||
}
|
||||
}
|
||||
utils.Info("批量删除完成:成功删除 %d 个资源,Meilisearch删除 %d 个资源", count, deletedCount)
|
||||
} else {
|
||||
utils.Error("批量删除时无法获取Meilisearch服务")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"deleted": count, "message": "批量删除成功"})
|
||||
}
|
||||
|
||||
|
||||
@@ -22,16 +22,20 @@ func GetStats(c *gin.Context) {
|
||||
db.DB.Model(&entity.Tag{}).Count(&totalTags)
|
||||
db.DB.Model(&entity.Resource{}).Select("COALESCE(SUM(view_count), 0)").Scan(&totalViews)
|
||||
|
||||
// 获取今日数据
|
||||
today := utils.GetTodayString()
|
||||
// 获取今日数据(在UTC+8时区的0点开始统计)
|
||||
now := utils.GetCurrentTime()
|
||||
// 使用UTC+8时区的今天0点
|
||||
loc, _ := time.LoadLocation("Asia/Shanghai") // UTC+8
|
||||
startOfToday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||
endOfToday := startOfToday.Add(24 * time.Hour)
|
||||
|
||||
// 今日新增资源数量
|
||||
// 今日新增资源数量(从0点开始)
|
||||
var todayResources int64
|
||||
db.DB.Model(&entity.Resource{}).Where("DATE(created_at) = ?", today).Count(&todayResources)
|
||||
db.DB.Model(&entity.Resource{}).Where("created_at >= ? AND created_at < ?", startOfToday, endOfToday).Count(&todayResources)
|
||||
|
||||
// 今日更新资源数量(包括新增和修改)
|
||||
// 今日更新资源数量(包括新增和修改,从0点开始)
|
||||
var todayUpdates int64
|
||||
db.DB.Model(&entity.Resource{}).Where("DATE(updated_at) = ?", today).Count(&todayUpdates)
|
||||
db.DB.Model(&entity.Resource{}).Where("updated_at >= ? AND updated_at < ?", startOfToday, endOfToday).Count(&todayUpdates)
|
||||
|
||||
// 今日浏览量 - 使用访问记录表统计今日访问量
|
||||
var todayViews int64
|
||||
@@ -41,9 +45,9 @@ func GetStats(c *gin.Context) {
|
||||
todayViews = 0
|
||||
}
|
||||
|
||||
// 今日搜索量
|
||||
// 今日搜索量(从0点开始)
|
||||
var todaySearches int64
|
||||
db.DB.Model(&entity.SearchStat{}).Where("DATE(date) = ?", today).Count(&todaySearches)
|
||||
db.DB.Model(&entity.SearchStat{}).Where("date >= ? AND date < ?", startOfToday.Format(utils.TimeFormatDate), endOfToday.Format(utils.TimeFormatDate)).Count(&todaySearches)
|
||||
|
||||
// 添加调试日志
|
||||
utils.Info("统计数据 - 总资源: %d, 总分类: %d, 总标签: %d, 总浏览量: %d",
|
||||
|
||||
@@ -125,10 +125,15 @@ func GetSystemConfig(c *gin.Context) {
|
||||
func UpdateSystemConfig(c *gin.Context) {
|
||||
var req dto.SystemConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.Error("JSON绑定失败: %v", err)
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("UpdateSystemConfig - 管理员更新系统配置 - 管理员: %s, IP: %s", adminUsername, clientIP)
|
||||
|
||||
// 调试信息
|
||||
utils.Info("接收到的配置请求: %+v", req)
|
||||
|
||||
@@ -141,30 +146,52 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 验证参数 - 只验证提交的字段
|
||||
if req.SiteTitle != nil && (len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100) {
|
||||
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
|
||||
return
|
||||
utils.Info("开始验证参数")
|
||||
if req.SiteTitle != nil {
|
||||
utils.Info("验证SiteTitle: '%s', 长度: %d", *req.SiteTitle, len(*req.SiteTitle))
|
||||
if len(*req.SiteTitle) < 1 || len(*req.SiteTitle) > 100 {
|
||||
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.AutoProcessInterval != nil && (*req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440) {
|
||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||
return
|
||||
if req.AutoProcessInterval != nil {
|
||||
utils.Info("验证AutoProcessInterval: %d", *req.AutoProcessInterval)
|
||||
if *req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440 {
|
||||
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.PageSize != nil && (*req.PageSize < 10 || *req.PageSize > 500) {
|
||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||
return
|
||||
if req.PageSize != nil {
|
||||
utils.Info("验证PageSize: %d", *req.PageSize)
|
||||
if *req.PageSize < 10 || *req.PageSize > 500 {
|
||||
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证自动转存配置
|
||||
if req.AutoTransferLimitDays != nil && (*req.AutoTransferLimitDays < 0 || *req.AutoTransferLimitDays > 365) {
|
||||
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
|
||||
return
|
||||
if req.AutoTransferLimitDays != nil {
|
||||
utils.Info("验证AutoTransferLimitDays: %d", *req.AutoTransferLimitDays)
|
||||
if *req.AutoTransferLimitDays < 0 || *req.AutoTransferLimitDays > 365 {
|
||||
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.AutoTransferMinSpace != nil && (*req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024) {
|
||||
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
||||
return
|
||||
if req.AutoTransferMinSpace != nil {
|
||||
utils.Info("验证AutoTransferMinSpace: %d", *req.AutoTransferMinSpace)
|
||||
if *req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024 {
|
||||
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 验证公告相关字段
|
||||
if req.Announcements != nil {
|
||||
utils.Info("验证Announcements: '%s'", *req.Announcements)
|
||||
// 可以在这里添加更详细的验证逻辑
|
||||
}
|
||||
|
||||
// 转换为实体
|
||||
@@ -297,6 +324,10 @@ func ToggleAutoProcess(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("ToggleAutoProcess - 管理员切换自动处理配置 - 管理员: %s, 启用: %t, IP: %s", adminUsername, req.AutoProcessReadyResources, clientIP)
|
||||
|
||||
// 获取当前配置
|
||||
configs, err := repoManager.SystemConfigRepository.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
|
||||
@@ -51,6 +51,10 @@ func (h *TaskHandler) CreateBatchTransferTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("CreateBatchTransferTask - 用户创建批量转存任务 - 用户: %s, 任务标题: %s, 资源数量: %d, IP: %s", username, req.Title, len(req.Resources), clientIP)
|
||||
|
||||
utils.Debug("创建批量转存任务: %s,资源数量: %d,选择账号数量: %d", req.Title, len(req.Resources), len(req.SelectedAccounts))
|
||||
|
||||
// 构建任务配置
|
||||
@@ -124,6 +128,10 @@ func (h *TaskHandler) StartTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("StartTask - 用户启动任务 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
|
||||
|
||||
err = h.taskManager.StartTask(uint(taskID))
|
||||
if err != nil {
|
||||
utils.Error("启动任务失败: %v", err)
|
||||
@@ -147,6 +155,10 @@ func (h *TaskHandler) StopTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("StopTask - 用户停止任务 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
|
||||
|
||||
err = h.taskManager.StopTask(uint(taskID))
|
||||
if err != nil {
|
||||
utils.Error("停止任务失败: %v", err)
|
||||
@@ -170,6 +182,10 @@ func (h *TaskHandler) PauseTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("PauseTask - 用户暂停任务 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
|
||||
|
||||
err = h.taskManager.PauseTask(uint(taskID))
|
||||
if err != nil {
|
||||
utils.Error("暂停任务失败: %v", err)
|
||||
@@ -360,8 +376,13 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("DeleteTask - 用户删除任务 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
|
||||
|
||||
// 检查任务是否在运行
|
||||
if h.taskManager.IsTaskRunning(uint(taskID)) {
|
||||
utils.Warn("DeleteTask - 尝试删除正在运行的任务 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
|
||||
ErrorResponse(c, "任务正在运行中,无法删除", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -383,6 +404,7 @@ func (h *TaskHandler) DeleteTask(c *gin.Context) {
|
||||
}
|
||||
|
||||
utils.Debug("任务删除成功: %d", taskID)
|
||||
utils.Info("DeleteTask - 任务删除成功 - 用户: %s, 任务ID: %d, IP: %s", username, taskID, clientIP)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "任务删除成功",
|
||||
@@ -402,6 +424,10 @@ func (h *TaskHandler) CreateExpansionTask(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("CreateExpansionTask - 用户创建扩容任务 - 用户: %s, 账号ID: %d, IP: %s", username, req.PanAccountID, clientIP)
|
||||
|
||||
utils.Debug("创建扩容任务: 账号ID %d", req.PanAccountID)
|
||||
|
||||
// 获取账号信息,用于构建任务标题
|
||||
|
||||
@@ -206,6 +206,9 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("[TELEGRAM:HANDLER] 接收到频道更新请求: ID=%s, ChatName=%s, PushStartTime=%s, PushEndTime=%s, ResourceStrategy=%s, TimeLimit=%s",
|
||||
idStr, req.ChatName, req.PushStartTime, req.PushEndTime, req.ResourceStrategy, req.TimeLimit)
|
||||
|
||||
// 查找现有频道
|
||||
channel, err := h.telegramChannelRepo.FindByID(uint(id))
|
||||
if err != nil {
|
||||
@@ -213,6 +216,10 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 保存前的日志
|
||||
utils.Info("[TELEGRAM:HANDLER] 更新前频道状态: PushStartTime=%s, PushEndTime=%s, ResourceStrategy=%s, TimeLimit=%s",
|
||||
channel.PushStartTime, channel.PushEndTime, channel.ResourceStrategy, channel.TimeLimit)
|
||||
|
||||
// 如果前端传递了ChatID,验证它是否与现有频道匹配
|
||||
if req.ChatID != 0 && req.ChatID != channel.ChatID {
|
||||
ErrorResponse(c, "ChatID不匹配,无法更新此频道", http.StatusBadRequest)
|
||||
@@ -229,12 +236,18 @@ func (h *TelegramHandler) UpdateChannel(c *gin.Context) {
|
||||
channel.ContentCategories = req.ContentCategories
|
||||
channel.ContentTags = req.ContentTags
|
||||
channel.IsActive = req.IsActive
|
||||
channel.ResourceStrategy = req.ResourceStrategy
|
||||
channel.TimeLimit = req.TimeLimit
|
||||
|
||||
if err := h.telegramChannelRepo.Update(channel); err != nil {
|
||||
ErrorResponse(c, "更新频道失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存后的日志
|
||||
utils.Info("[TELEGRAM:HANDLER] 更新后频道状态: PushStartTime=%s, PushEndTime=%s, ResourceStrategy=%s, TimeLimit=%s",
|
||||
channel.PushStartTime, channel.PushEndTime, channel.ResourceStrategy, channel.TimeLimit)
|
||||
|
||||
response := converter.TelegramChannelToResponse(*channel)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
@@ -278,13 +291,18 @@ func (h *TelegramHandler) RegisterChannelByCommand(chatID int64, chatName, chatT
|
||||
|
||||
// 创建新的频道记录
|
||||
channel := entity.TelegramChannel{
|
||||
ChatID: chatID,
|
||||
ChatName: chatName,
|
||||
ChatType: chatType,
|
||||
PushEnabled: true,
|
||||
PushFrequency: 5, // 默认5分钟
|
||||
IsActive: true,
|
||||
RegisteredBy: "bot_command",
|
||||
ChatID: chatID,
|
||||
ChatName: chatName,
|
||||
ChatType: chatType,
|
||||
PushEnabled: true,
|
||||
PushFrequency: 15, // 默认15分钟
|
||||
PushStartTime: "08:30", // 默认开始时间8:30
|
||||
PushEndTime: "11:30", // 默认结束时间11:30
|
||||
IsActive: true,
|
||||
RegisteredBy: "bot_command",
|
||||
RegisteredAt: time.Now(),
|
||||
ResourceStrategy: "random", // 默认纯随机
|
||||
TimeLimit: "none", // 默认无限制
|
||||
}
|
||||
|
||||
return h.telegramChannelRepo.Create(&channel)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/middleware"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -20,18 +22,24 @@ func Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("Login - 尝试登录 - 用户名: %s, IP: %s", req.Username, clientIP)
|
||||
|
||||
user, err := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if err != nil {
|
||||
utils.Warn("Login - 用户不存在或密码错误 - 用户名: %s, IP: %s", req.Username, clientIP)
|
||||
ErrorResponse(c, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsActive {
|
||||
utils.Warn("Login - 账户已被禁用 - 用户名: %s, IP: %s", req.Username, clientIP)
|
||||
ErrorResponse(c, "账户已被禁用", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !middleware.CheckPassword(req.Password, user.Password) {
|
||||
utils.Warn("Login - 密码错误 - 用户名: %s, IP: %s", req.Username, clientIP)
|
||||
ErrorResponse(c, "用户名或密码错误", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -42,10 +50,13 @@ func Login(c *gin.Context) {
|
||||
// 生成JWT令牌
|
||||
token, err := middleware.GenerateToken(user)
|
||||
if err != nil {
|
||||
utils.Error("Login - 生成令牌失败 - 用户名: %s, IP: %s, Error: %v", req.Username, clientIP, err)
|
||||
ErrorResponse(c, "生成令牌失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("Login - 登录成功 - 用户名: %s(ID:%d), IP: %s", req.Username, user.ID, clientIP)
|
||||
|
||||
response := dto.LoginResponse{
|
||||
Token: token,
|
||||
User: converter.ToUserResponse(user),
|
||||
@@ -62,9 +73,13 @@ func Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("Register - 尝试注册 - 用户名: %s, 邮箱: %s, IP: %s", req.Username, req.Email, clientIP)
|
||||
|
||||
// 检查用户名是否已存在
|
||||
existingUser, _ := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if existingUser != nil {
|
||||
utils.Warn("Register - 用户名已存在 - 用户名: %s, IP: %s", req.Username, clientIP)
|
||||
ErrorResponse(c, "用户名已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -72,6 +87,7 @@ func Register(c *gin.Context) {
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, _ := repoManager.UserRepository.FindByEmail(req.Email)
|
||||
if existingEmail != nil {
|
||||
utils.Warn("Register - 邮箱已存在 - 邮箱: %s, IP: %s", req.Email, clientIP)
|
||||
ErrorResponse(c, "邮箱已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -79,6 +95,7 @@ func Register(c *gin.Context) {
|
||||
// 哈希密码
|
||||
hashedPassword, err := middleware.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
utils.Error("Register - 密码加密失败 - 用户名: %s, IP: %s, Error: %v", req.Username, clientIP, err)
|
||||
ErrorResponse(c, "密码加密失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -93,10 +110,13 @@ func Register(c *gin.Context) {
|
||||
|
||||
err = repoManager.UserRepository.Create(user)
|
||||
if err != nil {
|
||||
utils.Error("Register - 创建用户失败 - 用户名: %s, IP: %s, Error: %v", req.Username, clientIP, err)
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("Register - 注册成功 - 用户名: %s(ID:%d), 邮箱: %s, IP: %s", req.Username, user.ID, req.Email, clientIP)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "注册成功",
|
||||
"user": converter.ToUserResponse(user),
|
||||
@@ -123,9 +143,14 @@ func CreateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("CreateUser - 管理员创建用户 - 管理员: %s, 新用户名: %s, IP: %s", adminUsername, req.Username, clientIP)
|
||||
|
||||
// 检查用户名是否已存在
|
||||
existingUser, _ := repoManager.UserRepository.FindByUsername(req.Username)
|
||||
if existingUser != nil {
|
||||
utils.Warn("CreateUser - 用户名已存在 - 管理员: %s, 用户名: %s, IP: %s", adminUsername, req.Username, clientIP)
|
||||
ErrorResponse(c, "用户名已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -133,6 +158,7 @@ func CreateUser(c *gin.Context) {
|
||||
// 检查邮箱是否已存在
|
||||
existingEmail, _ := repoManager.UserRepository.FindByEmail(req.Email)
|
||||
if existingEmail != nil {
|
||||
utils.Warn("CreateUser - 邮箱已存在 - 管理员: %s, 邮箱: %s, IP: %s", adminUsername, req.Email, clientIP)
|
||||
ErrorResponse(c, "邮箱已存在", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
@@ -140,6 +166,7 @@ func CreateUser(c *gin.Context) {
|
||||
// 哈希密码
|
||||
hashedPassword, err := middleware.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
utils.Error("CreateUser - 密码加密失败 - 管理员: %s, 用户名: %s, IP: %s, Error: %v", adminUsername, req.Username, clientIP, err)
|
||||
ErrorResponse(c, "密码加密失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -154,10 +181,13 @@ func CreateUser(c *gin.Context) {
|
||||
|
||||
err = repoManager.UserRepository.Create(user)
|
||||
if err != nil {
|
||||
utils.Error("CreateUser - 创建用户失败 - 管理员: %s, 用户名: %s, IP: %s, Error: %v", adminUsername, req.Username, clientIP, err)
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("CreateUser - 用户创建成功 - 管理员: %s, 用户名: %s(ID:%d), 角色: %s, IP: %s", adminUsername, req.Username, user.ID, req.Role, clientIP)
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "用户创建成功",
|
||||
"user": converter.ToUserResponse(user),
|
||||
@@ -179,12 +209,21 @@ func UpdateUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("UpdateUser - 管理员更新用户 - 管理员: %s, 目标用户ID: %d, IP: %s", adminUsername, id, clientIP)
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
utils.Warn("UpdateUser - 目标用户不存在 - 管理员: %s, 用户ID: %d, IP: %s", adminUsername, id, clientIP)
|
||||
ErrorResponse(c, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录变更前的信息
|
||||
oldInfo := fmt.Sprintf("用户名:%s,邮箱:%s,角色:%s,状态:%t", user.Username, user.Email, user.Role, user.IsActive)
|
||||
utils.Debug("UpdateUser - 更新前用户信息 - 管理员: %s, 用户ID: %d, 信息: %s", adminUsername, id, oldInfo)
|
||||
|
||||
if req.Username != "" {
|
||||
user.Username = req.Username
|
||||
}
|
||||
@@ -198,10 +237,15 @@ func UpdateUser(c *gin.Context) {
|
||||
|
||||
err = repoManager.UserRepository.Update(user)
|
||||
if err != nil {
|
||||
utils.Error("UpdateUser - 更新用户失败 - 管理员: %s, 用户ID: %d, IP: %s, Error: %v", adminUsername, id, clientIP, err)
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录变更后信息
|
||||
newInfo := fmt.Sprintf("用户名:%s,邮箱:%s,角色:%s,状态:%t", user.Username, user.Email, user.Role, user.IsActive)
|
||||
utils.Info("UpdateUser - 用户更新成功 - 管理员: %s, 用户ID: %d, 更新前: %s, 更新后: %s, IP: %s", adminUsername, id, oldInfo, newInfo, clientIP)
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "用户更新成功"})
|
||||
}
|
||||
|
||||
@@ -220,8 +264,13 @@ func ChangePassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("ChangePassword - 管理员修改用户密码 - 管理员: %s, 目标用户ID: %d, IP: %s", adminUsername, id, clientIP)
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
utils.Warn("ChangePassword - 目标用户不存在 - 管理员: %s, 用户ID: %d, IP: %s", adminUsername, id, clientIP)
|
||||
ErrorResponse(c, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -229,6 +278,7 @@ func ChangePassword(c *gin.Context) {
|
||||
// 哈希新密码
|
||||
hashedPassword, err := middleware.HashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
utils.Error("ChangePassword - 密码加密失败 - 管理员: %s, 用户ID: %d, IP: %s, Error: %v", adminUsername, id, clientIP, err)
|
||||
ErrorResponse(c, "密码加密失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -236,10 +286,13 @@ func ChangePassword(c *gin.Context) {
|
||||
user.Password = hashedPassword
|
||||
err = repoManager.UserRepository.Update(user)
|
||||
if err != nil {
|
||||
utils.Error("ChangePassword - 更新密码失败 - 管理员: %s, 用户ID: %d, IP: %s, Error: %v", adminUsername, id, clientIP, err)
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("ChangePassword - 密码修改成功 - 管理员: %s, 用户名: %s(ID:%d), IP: %s", adminUsername, user.Username, id, clientIP)
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "密码修改成功"})
|
||||
}
|
||||
|
||||
@@ -252,12 +305,27 @@ func DeleteUser(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
adminUsername, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("DeleteUser - 管理员删除用户 - 管理员: %s, 目标用户ID: %d, IP: %s", adminUsername, id, clientIP)
|
||||
|
||||
// 先获取用户信息用于日志记录
|
||||
user, err := repoManager.UserRepository.FindByID(uint(id))
|
||||
if err != nil {
|
||||
utils.Warn("DeleteUser - 目标用户不存在 - 管理员: %s, 用户ID: %d, IP: %s", adminUsername, id, clientIP)
|
||||
ErrorResponse(c, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
err = repoManager.UserRepository.Delete(uint(id))
|
||||
if err != nil {
|
||||
utils.Error("DeleteUser - 删除用户失败 - 管理员: %s, 用户ID: %d, IP: %s, Error: %v", adminUsername, id, clientIP, err)
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("DeleteUser - 用户删除成功 - 管理员: %s, 用户名: %s(ID:%d), IP: %s", adminUsername, user.Username, id, clientIP)
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "用户删除成功"})
|
||||
}
|
||||
|
||||
@@ -269,12 +337,18 @@ func GetProfile(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
utils.Info("GetProfile - 用户获取个人资料 - 用户名: %s(ID:%d), IP: %s", username, userID, clientIP)
|
||||
|
||||
user, err := repoManager.UserRepository.FindByID(userID.(uint))
|
||||
if err != nil {
|
||||
utils.Warn("GetProfile - 用户不存在 - 用户名: %s(ID:%d), IP: %s", username, userID, clientIP)
|
||||
ErrorResponse(c, "用户不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := converter.ToUserResponse(user)
|
||||
utils.Debug("GetProfile - 成功获取个人资料 - 用户名: %s(ID:%d), IP: %s", username, userID, clientIP)
|
||||
SuccessResponse(c, response)
|
||||
}
|
||||
|
||||
286
handlers/wechat_handler.go
Normal file
286
handlers/wechat_handler.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/silenceper/wechat/v2/officialaccount/message"
|
||||
)
|
||||
|
||||
// WechatHandler 微信公众号处理器
|
||||
type WechatHandler struct {
|
||||
wechatService services.WechatBotService
|
||||
systemConfigRepo repo.SystemConfigRepository
|
||||
}
|
||||
|
||||
// NewWechatHandler 创建微信公众号处理器
|
||||
func NewWechatHandler(
|
||||
wechatService services.WechatBotService,
|
||||
systemConfigRepo repo.SystemConfigRepository,
|
||||
) *WechatHandler {
|
||||
return &WechatHandler{
|
||||
wechatService: wechatService,
|
||||
systemConfigRepo: systemConfigRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWechatMessage 处理微信消息推送
|
||||
func (h *WechatHandler) HandleWechatMessage(c *gin.Context) {
|
||||
// 验证微信消息签名
|
||||
if !h.validateSignature(c) {
|
||||
utils.Error("[WECHAT:VALIDATE] 签名验证失败")
|
||||
c.String(http.StatusForbidden, "签名验证失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 处理微信验证请求
|
||||
if c.Request.Method == "GET" {
|
||||
echostr := c.Query("echostr")
|
||||
utils.Info("[WECHAT:VERIFY] 微信服务器验证成功, echostr=%s", echostr)
|
||||
c.String(http.StatusOK, echostr)
|
||||
return
|
||||
}
|
||||
|
||||
// 读取请求体
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
utils.Error("[WECHAT:MESSAGE] 读取请求体失败: %v", err)
|
||||
c.String(http.StatusBadRequest, "读取请求体失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析微信消息
|
||||
var msg message.MixMessage
|
||||
if err := xml.Unmarshal(body, &msg); err != nil {
|
||||
utils.Error("[WECHAT:MESSAGE] 解析微信消息失败: %v", err)
|
||||
c.String(http.StatusBadRequest, "消息格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
reply, err := h.wechatService.HandleMessage(&msg)
|
||||
if err != nil {
|
||||
utils.Error("[WECHAT:MESSAGE] 处理微信消息失败: %v", err)
|
||||
c.String(http.StatusInternalServerError, "处理失败")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("[WECHAT:MESSAGE] 回复对象: %v", reply)
|
||||
|
||||
// 如果有回复内容,发送回复
|
||||
if reply != nil {
|
||||
// 为微信消息设置正确的ToUserName和FromUserName
|
||||
switch v := reply.(type) {
|
||||
case *message.Text:
|
||||
if v.CommonToken.ToUserName == "" {
|
||||
v.CommonToken.ToUserName = msg.FromUserName
|
||||
}
|
||||
if v.CommonToken.FromUserName == "" {
|
||||
v.CommonToken.FromUserName = msg.ToUserName
|
||||
}
|
||||
if v.CommonToken.CreateTime == 0 {
|
||||
v.CommonToken.CreateTime = time.Now().Unix()
|
||||
}
|
||||
// 确保MsgType正确设置
|
||||
if v.CommonToken.MsgType == "" {
|
||||
v.CommonToken.MsgType = message.MsgTypeText
|
||||
}
|
||||
case *message.Image:
|
||||
if v.CommonToken.ToUserName == "" {
|
||||
v.CommonToken.ToUserName = msg.FromUserName
|
||||
}
|
||||
if v.CommonToken.FromUserName == "" {
|
||||
v.CommonToken.FromUserName = msg.ToUserName
|
||||
}
|
||||
if v.CommonToken.CreateTime == 0 {
|
||||
v.CommonToken.CreateTime = time.Now().Unix()
|
||||
}
|
||||
// 确保MsgType正确设置
|
||||
if v.CommonToken.MsgType == "" {
|
||||
v.CommonToken.MsgType = message.MsgTypeImage
|
||||
}
|
||||
}
|
||||
|
||||
responseXML, err := xml.Marshal(reply)
|
||||
if err != nil {
|
||||
utils.Error("[WECHAT:MESSAGE] 序列化回复消息失败: %v", err)
|
||||
c.String(http.StatusInternalServerError, "回复失败")
|
||||
return
|
||||
}
|
||||
utils.Info("[WECHAT:MESSAGE] 回复XML: %s", string(responseXML))
|
||||
c.Data(http.StatusOK, "application/xml", responseXML)
|
||||
} else {
|
||||
utils.Warn("[WECHAT:MESSAGE] 没有回复内容,返回success")
|
||||
c.String(http.StatusOK, "success")
|
||||
}
|
||||
}
|
||||
|
||||
// GetBotConfig 获取微信机器人配置
|
||||
func (h *WechatHandler) GetBotConfig(c *gin.Context) {
|
||||
configs, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
botConfig := converter.SystemConfigToWechatBotConfig(configs)
|
||||
SuccessResponse(c, botConfig)
|
||||
}
|
||||
|
||||
// UpdateBotConfig 更新微信机器人配置
|
||||
func (h *WechatHandler) UpdateBotConfig(c *gin.Context) {
|
||||
var req dto.WechatBotConfigRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为系统配置实体
|
||||
configs := converter.WechatBotConfigRequestToSystemConfigs(req)
|
||||
|
||||
// 保存配置
|
||||
if len(configs) > 0 {
|
||||
err := h.systemConfigRepo.UpsertConfigs(configs)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "保存配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 重新加载配置缓存
|
||||
if err := h.systemConfigRepo.SafeRefreshConfigCache(); err != nil {
|
||||
ErrorResponse(c, "刷新配置缓存失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新加载机器人服务配置
|
||||
if err := h.wechatService.ReloadConfig(); err != nil {
|
||||
ErrorResponse(c, "重新加载机器人配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 配置更新完成后,尝试启动机器人(如果未运行且配置有效)
|
||||
if startErr := h.wechatService.Start(); startErr != nil {
|
||||
utils.Warn("[WECHAT:HANDLER] 配置更新后尝试启动机器人失败: %v", startErr)
|
||||
// 启动失败不影响配置保存,只记录警告
|
||||
}
|
||||
|
||||
// 返回成功
|
||||
SuccessResponse(c, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "配置更新成功,机器人已尝试启动",
|
||||
})
|
||||
}
|
||||
|
||||
// GetBotStatus 获取机器人状态
|
||||
func (h *WechatHandler) GetBotStatus(c *gin.Context) {
|
||||
// 获取机器人运行时状态
|
||||
runtimeStatus := h.wechatService.GetRuntimeStatus()
|
||||
|
||||
// 获取配置状态
|
||||
configs, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取配置失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析配置状态
|
||||
configStatus := map[string]interface{}{
|
||||
"enabled": false,
|
||||
"auto_reply_enabled": false,
|
||||
"app_id_configured": false,
|
||||
"token_configured": false,
|
||||
}
|
||||
|
||||
for _, config := range configs {
|
||||
switch config.Key {
|
||||
case entity.ConfigKeyWechatBotEnabled:
|
||||
configStatus["enabled"] = config.Value == "true"
|
||||
case entity.ConfigKeyWechatAutoReplyEnabled:
|
||||
configStatus["auto_reply_enabled"] = config.Value == "true"
|
||||
case entity.ConfigKeyWechatAppId:
|
||||
configStatus["app_id_configured"] = config.Value != ""
|
||||
case entity.ConfigKeyWechatToken:
|
||||
configStatus["token_configured"] = config.Value != ""
|
||||
}
|
||||
}
|
||||
|
||||
// 合并状态信息
|
||||
status := map[string]interface{}{
|
||||
"config": configStatus,
|
||||
"runtime": runtimeStatus,
|
||||
"overall_status": runtimeStatus["is_running"].(bool),
|
||||
"status_text": func() string {
|
||||
if runtimeStatus["is_running"].(bool) {
|
||||
return "运行中"
|
||||
} else if configStatus["enabled"].(bool) {
|
||||
return "已启用但未运行"
|
||||
} else {
|
||||
return "已停止"
|
||||
}
|
||||
}(),
|
||||
}
|
||||
|
||||
SuccessResponse(c, status)
|
||||
}
|
||||
|
||||
// validateSignature 验证微信消息签名
|
||||
func (h *WechatHandler) validateSignature(c *gin.Context) bool {
|
||||
// 获取配置中的Token
|
||||
configs, err := h.systemConfigRepo.GetOrCreateDefault()
|
||||
if err != nil {
|
||||
utils.Error("[WECHAT:VALIDATE] 获取配置失败: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
var token string
|
||||
for _, config := range configs {
|
||||
if config.Key == entity.ConfigKeyWechatToken {
|
||||
token = config.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
utils.Debug("[WECHAT:VALIDATE] Token配置状态: %t", token != "")
|
||||
|
||||
if token == "" {
|
||||
// 如果没有配置Token,跳过签名验证(开发模式)
|
||||
utils.Warn("[WECHAT:VALIDATE] 未配置Token,跳过签名验证")
|
||||
return true
|
||||
}
|
||||
|
||||
signature := c.Query("signature")
|
||||
timestamp := c.Query("timestamp")
|
||||
nonce := c.Query("nonce")
|
||||
|
||||
utils.Debug("[WECHAT:VALIDATE] 接收到的参数 - signature: %s, timestamp: %s, nonce: %s", signature, timestamp, nonce)
|
||||
|
||||
// 验证签名
|
||||
tmpArr := []string{token, timestamp, nonce}
|
||||
sort.Strings(tmpArr)
|
||||
tmpStr := strings.Join(tmpArr, "")
|
||||
tmpStr = fmt.Sprintf("%x", sha1.Sum([]byte(tmpStr)))
|
||||
|
||||
utils.Debug("[WECHAT:VALIDATE] 计算出的签名: %s, 微信提供的签名: %s", tmpStr, signature)
|
||||
|
||||
if tmpStr == signature {
|
||||
utils.Info("[WECHAT:VALIDATE] 签名验证成功")
|
||||
return true
|
||||
} else {
|
||||
utils.Error("[WECHAT:VALIDATE] 签名验证失败 - 计算出的签名: %s, 微信提供的签名: %s", tmpStr, signature)
|
||||
return false
|
||||
}
|
||||
}
|
||||
164
main.go
164
main.go
@@ -5,12 +5,17 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/config"
|
||||
"github.com/ctwj/urldb/db"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/handlers"
|
||||
"github.com/ctwj/urldb/middleware"
|
||||
"github.com/ctwj/urldb/monitor"
|
||||
"github.com/ctwj/urldb/plugin"
|
||||
_ "github.com/ctwj/urldb/plugin/demo" // 导入demo包以触发插件自动注册
|
||||
"github.com/ctwj/urldb/scheduler"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/task"
|
||||
@@ -38,7 +43,6 @@ func main() {
|
||||
if err := utils.InitLogger(nil); err != nil {
|
||||
log.Fatal("初始化日志系统失败:", err)
|
||||
}
|
||||
defer utils.GetLogger().Close()
|
||||
|
||||
// 加载环境变量
|
||||
if err := godotenv.Load(); err != nil {
|
||||
@@ -85,9 +89,30 @@ func main() {
|
||||
// 创建Repository管理器
|
||||
repoManager := repo.NewRepositoryManager(db.DB)
|
||||
|
||||
// 创建配置管理器
|
||||
configManager := config.NewConfigManager(repoManager)
|
||||
|
||||
// 设置全局配置管理器
|
||||
config.SetGlobalConfigManager(configManager)
|
||||
|
||||
// 加载所有配置到缓存
|
||||
if err := configManager.LoadAllConfigs(); err != nil {
|
||||
utils.Error("加载配置缓存失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建任务管理器
|
||||
taskManager := task.NewTaskManager(repoManager)
|
||||
|
||||
// 初始化插件系统
|
||||
plugin.InitPluginSystem(taskManager, repoManager)
|
||||
|
||||
// 注册demo插件
|
||||
registerDemoPlugins()
|
||||
|
||||
// 初始化插件监控系统
|
||||
// pluginMonitor := monitor.NewPluginMonitor()
|
||||
// pluginHealthChecker := monitor.NewPluginHealthChecker(pluginMonitor)
|
||||
|
||||
// 注册转存任务处理器
|
||||
transferProcessor := task.NewTransferProcessor(repoManager)
|
||||
taskManager.RegisterProcessor(transferProcessor)
|
||||
@@ -112,7 +137,22 @@ func main() {
|
||||
utils.Info("任务管理器初始化完成")
|
||||
|
||||
// 创建Gin实例
|
||||
r := gin.Default()
|
||||
r := gin.New()
|
||||
|
||||
// 创建监控和错误处理器
|
||||
metrics := monitor.GetGlobalMetrics()
|
||||
errorHandler := monitor.GetGlobalErrorHandler()
|
||||
if errorHandler == nil {
|
||||
errorHandler = monitor.NewErrorHandler(1000, 24*time.Hour)
|
||||
monitor.SetGlobalErrorHandler(errorHandler)
|
||||
}
|
||||
|
||||
// 添加中间件
|
||||
r.Use(gin.Logger()) // Gin日志中间件
|
||||
r.Use(errorHandler.RecoverMiddleware()) // Panic恢复中间件
|
||||
r.Use(errorHandler.ErrorMiddleware()) // 错误处理中间件
|
||||
r.Use(metrics.MetricsMiddleware()) // 监控中间件
|
||||
r.Use(gin.Recovery()) // Gin恢复中间件
|
||||
|
||||
// 配置CORS
|
||||
config := cors.DefaultConfig()
|
||||
@@ -124,9 +164,15 @@ func main() {
|
||||
// 将Repository管理器注入到handlers中
|
||||
handlers.SetRepositoryManager(repoManager)
|
||||
|
||||
// 将Repository管理器注入到services中
|
||||
services.SetRepositoryManager(repoManager)
|
||||
|
||||
// 设置Meilisearch管理器到handlers中
|
||||
handlers.SetMeilisearchManager(meilisearchManager)
|
||||
|
||||
// 设置Meilisearch管理器到services中
|
||||
services.SetMeilisearchManager(meilisearchManager)
|
||||
|
||||
// 设置全局调度器的Meilisearch管理器
|
||||
scheduler.SetGlobalMeilisearchManager(meilisearchManager)
|
||||
|
||||
@@ -272,6 +318,18 @@ func main() {
|
||||
api.POST("/search-stats/record", handlers.RecordSearch)
|
||||
api.GET("/search-stats/summary", handlers.GetSearchStatsSummary)
|
||||
|
||||
// API访问日志路由
|
||||
api.GET("/api-access-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogs)
|
||||
api.GET("/api-access-logs/summary", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogSummary)
|
||||
api.GET("/api-access-logs/stats", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetAPIAccessLogStats)
|
||||
api.DELETE("/api-access-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearAPIAccessLogs)
|
||||
|
||||
// 系统日志路由
|
||||
api.GET("/system-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemLogs)
|
||||
api.GET("/system-logs/files", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemLogFiles)
|
||||
api.GET("/system-logs/summary", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemLogSummary)
|
||||
api.DELETE("/system-logs", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.ClearSystemLogs)
|
||||
|
||||
// 系统配置路由
|
||||
api.GET("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetSystemConfig)
|
||||
api.POST("/system/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.UpdateSystemConfig)
|
||||
@@ -323,6 +381,8 @@ func main() {
|
||||
api.GET("/files", middleware.AuthMiddleware(), fileHandler.GetFileList)
|
||||
api.DELETE("/files", middleware.AuthMiddleware(), fileHandler.DeleteFiles)
|
||||
api.PUT("/files", middleware.AuthMiddleware(), fileHandler.UpdateFile)
|
||||
// 微信公众号验证文件上传(无需认证,仅支持TXT文件)
|
||||
api.POST("/wechat/verify-file", fileHandler.UploadWechatVerifyFile)
|
||||
|
||||
// 创建Telegram Bot服务
|
||||
telegramBotService := services.NewTelegramBotService(
|
||||
@@ -337,6 +397,18 @@ func main() {
|
||||
utils.Error("启动Telegram Bot服务失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建微信公众号机器人服务
|
||||
wechatBotService := services.NewWechatBotService(
|
||||
repoManager.SystemConfigRepository,
|
||||
repoManager.ResourceRepository,
|
||||
repoManager.ReadyResourceRepository,
|
||||
)
|
||||
|
||||
// 启动微信公众号机器人服务
|
||||
if err := wechatBotService.Start(); err != nil {
|
||||
utils.Error("启动微信公众号机器人服务失败: %v", err)
|
||||
}
|
||||
|
||||
// Telegram相关路由
|
||||
telegramHandler := handlers.NewTelegramHandler(
|
||||
repoManager.TelegramChannelRepository,
|
||||
@@ -358,8 +430,69 @@ func main() {
|
||||
api.GET("/telegram/logs/stats", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.GetTelegramLogStats)
|
||||
api.POST("/telegram/logs/clear", middleware.AuthMiddleware(), middleware.AdminMiddleware(), telegramHandler.ClearTelegramLogs)
|
||||
api.POST("/telegram/webhook", telegramHandler.HandleWebhook)
|
||||
|
||||
// 微信公众号相关路由
|
||||
wechatHandler := handlers.NewWechatHandler(
|
||||
wechatBotService,
|
||||
repoManager.SystemConfigRepository,
|
||||
)
|
||||
api.GET("/wechat/bot-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), wechatHandler.GetBotConfig)
|
||||
api.PUT("/wechat/bot-config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), wechatHandler.UpdateBotConfig)
|
||||
api.GET("/wechat/bot-status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), wechatHandler.GetBotStatus)
|
||||
api.POST("/wechat/callback", wechatHandler.HandleWechatMessage)
|
||||
api.GET("/wechat/callback", wechatHandler.HandleWechatMessage)
|
||||
|
||||
// 插件管理相关路由
|
||||
pluginHandler := handlers.NewPluginHandler()
|
||||
api.GET("/plugins", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.GetPlugins)
|
||||
api.GET("/plugins/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.GetPlugin)
|
||||
api.POST("/plugins/:name/initialize", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.InitializePlugin)
|
||||
api.POST("/plugins/:name/start", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.StartPlugin)
|
||||
api.POST("/plugins/:name/stop", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.StopPlugin)
|
||||
api.DELETE("/plugins/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.UninstallPlugin)
|
||||
api.GET("/plugins/:name/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.GetPluginConfig)
|
||||
api.PUT("/plugins/:name/config", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.UpdatePluginConfig)
|
||||
api.GET("/plugins/:name/dependencies", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.GetPluginDependencies)
|
||||
api.GET("/plugins/load-order", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.GetPluginLoadOrder)
|
||||
api.POST("/plugins/validate-dependencies", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginHandler.ValidatePluginDependencies)
|
||||
|
||||
// 管理员插件管理界面路由
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.AuthMiddleware(), middleware.AdminMiddleware())
|
||||
{
|
||||
admin.GET("/plugins", pluginHandler.GetPlugins)
|
||||
admin.POST("/plugins/:name/start", pluginHandler.StartPlugin)
|
||||
admin.POST("/plugins/:name/stop", pluginHandler.StopPlugin)
|
||||
admin.POST("/plugins/:name/install", pluginHandler.InstallPlugin)
|
||||
admin.DELETE("/plugins/:name/uninstall", pluginHandler.UninstallPlugin)
|
||||
}
|
||||
|
||||
// 插件监控相关路由
|
||||
pluginMonitorHandler := handlers.NewPluginMonitorHandler()
|
||||
api.GET("/plugins/health/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetPluginHealth)
|
||||
api.GET("/plugins/health", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetAllPluginsHealth)
|
||||
api.GET("/plugins/activities/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetPluginActivities)
|
||||
api.GET("/plugins/metrics/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetPluginMetrics)
|
||||
api.GET("/plugins/monitor/stats", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetPluginMonitorStats)
|
||||
api.GET("/plugins/alerts/rules", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetAlertRules)
|
||||
api.POST("/plugins/alerts/rules", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.CreateAlertRule)
|
||||
api.DELETE("/plugins/alerts/rules/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.DeleteAlertRule)
|
||||
api.GET("/plugins/health-history/:name", middleware.AuthMiddleware(), middleware.AdminMiddleware(), pluginMonitorHandler.GetPluginHealthHistory)
|
||||
}
|
||||
|
||||
// 设置监控系统
|
||||
monitor.SetupMonitoring(r)
|
||||
|
||||
// 启动监控服务器
|
||||
metricsConfig := &monitor.MetricsConfig{
|
||||
Enabled: true,
|
||||
ListenAddress: ":9090",
|
||||
MetricsPath: "/metrics",
|
||||
Namespace: "urldb",
|
||||
Subsystem: "api",
|
||||
}
|
||||
metrics.StartMetricsServer(metricsConfig)
|
||||
|
||||
// 静态文件服务
|
||||
r.Static("/uploads", "./uploads")
|
||||
|
||||
@@ -381,3 +514,30 @@ func main() {
|
||||
utils.Info("服务器启动在端口 %s", port)
|
||||
r.Run(":" + port)
|
||||
}
|
||||
|
||||
// registerDemoPlugins 注册demo插件
|
||||
func registerDemoPlugins() {
|
||||
if plugin.GetManager() != nil {
|
||||
utils.Info("Plugin manager is ready, registering demo plugins")
|
||||
// 临时解决方案:直接在main中创建一些简单的插件
|
||||
registerBuiltinPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
// registerBuiltinPlugins 注册内置插件
|
||||
func registerBuiltinPlugins() {
|
||||
utils.Info("Registering builtin plugins...")
|
||||
|
||||
if plugin.GetManager() != nil {
|
||||
// 注册内置插件
|
||||
builtinPlugin := NewBuiltinPlugin()
|
||||
if err := plugin.GetManager().RegisterPlugin(builtinPlugin); err != nil {
|
||||
utils.Error("Failed to register builtin plugin: %v", err)
|
||||
} else {
|
||||
utils.Info("Successfully registered builtin plugin: %s", builtinPlugin.Name())
|
||||
}
|
||||
|
||||
pluginCount := len(plugin.GetManager().ListPlugins())
|
||||
utils.Info("Plugin system now has %d plugins registered", pluginCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,14 @@ type Claims struct {
|
||||
func AuthMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
// utils.Info("AuthMiddleware - 收到请求: %s %s", c.Request.Method, c.Request.URL.Path)
|
||||
// utils.Info("AuthMiddleware - Authorization头: %s", authHeader)
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.Request.UserAgent()
|
||||
|
||||
utils.Debug("AuthMiddleware - 认证请求: %s %s, IP: %s, UserAgent: %s",
|
||||
c.Request.Method, c.Request.URL.Path, clientIP, userAgent)
|
||||
|
||||
if authHeader == "" {
|
||||
utils.Error("AuthMiddleware - 未提供认证令牌")
|
||||
utils.Warn("AuthMiddleware - 未提供认证令牌 - IP: %s, Path: %s", clientIP, c.Request.URL.Path)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
|
||||
c.Abort()
|
||||
return
|
||||
@@ -39,29 +42,31 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
// utils.Error("AuthMiddleware - 无效的认证格式: %s", authHeader)
|
||||
utils.Warn("AuthMiddleware - 无效的认证格式 - IP: %s, Header: %s", clientIP, authHeader)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的认证格式"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
// utils.Info("AuthMiddleware - 解析令牌: %s", tokenString[:10]+"...")
|
||||
utils.Debug("AuthMiddleware - 解析令牌: %s...", tokenString[:utils.Min(len(tokenString), 10)])
|
||||
|
||||
claims, err := parseToken(tokenString)
|
||||
if err != nil {
|
||||
// utils.Error("AuthMiddleware - 令牌解析失败: %v", err)
|
||||
utils.Warn("AuthMiddleware - 令牌解析失败 - IP: %s, Error: %v", clientIP, err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的令牌"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// utils.Info("AuthMiddleware - 令牌验证成功,用户: %s, 角色: %s", claims.Username, claims.Role)
|
||||
utils.Info("AuthMiddleware - 认证成功 - 用户: %s(ID:%d), 角色: %s, IP: %s",
|
||||
claims.Username, claims.UserID, claims.Role, clientIP)
|
||||
|
||||
// 将用户信息存储到上下文中
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
c.Set("client_ip", clientIP)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
@@ -71,18 +76,23 @@ func AuthMiddleware() gin.HandlerFunc {
|
||||
func AdminMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
username, _ := c.Get("username")
|
||||
clientIP, _ := c.Get("client_ip")
|
||||
|
||||
if !exists {
|
||||
// c.JSON(http.StatusUnauthorized, gin.H{"error": "未认证"})
|
||||
utils.Warn("AdminMiddleware - 未认证访问管理员接口 - IP: %s, Path: %s", clientIP, c.Request.URL.Path)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if role != "admin" {
|
||||
// c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||||
utils.Warn("AdminMiddleware - 非管理员用户尝试访问管理员接口 - 用户: %s, 角色: %s, IP: %s, Path: %s",
|
||||
username, role, clientIP, c.Request.URL.Path)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("AdminMiddleware - 管理员访问接口 - 用户: %s, IP: %s, Path: %s", username, clientIP, c.Request.URL.Path)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ CREATE TABLE telegram_channels (
|
||||
token VARCHAR(255) COMMENT '访问令牌',
|
||||
api_type VARCHAR(50) COMMENT 'API类型',
|
||||
is_push_saved_info BOOLEAN DEFAULT FALSE COMMENT '是否只推送已转存资源',
|
||||
|
||||
-- 资源策略和时间限制配置
|
||||
resource_strategy VARCHAR(20) DEFAULT 'random' COMMENT '资源策略:latest-最新优先,transferred-已转存优先,random-纯随机',
|
||||
time_limit VARCHAR(20) DEFAULT 'none' COMMENT '时间限制:none-无限制,week-一周内,month-一月内',
|
||||
push_start_time VARCHAR(10) COMMENT '推送开始时间,格式HH:mm',
|
||||
push_end_time VARCHAR(10) COMMENT '推送结束时间,格式HH:mm',
|
||||
|
||||
-- 索引
|
||||
INDEX idx_chat_id (chat_id),
|
||||
|
||||
327
monitor/error_handler.go
Normal file
327
monitor/error_handler.go
Normal file
@@ -0,0 +1,327 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ErrorInfo 错误信息结构
|
||||
type ErrorInfo struct {
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Message string `json:"message"`
|
||||
StackTrace string `json:"stack_trace"`
|
||||
RequestInfo *RequestInfo `json:"request_info,omitempty"`
|
||||
Level string `json:"level"` // error, warn, info
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// RequestInfo 请求信息结构
|
||||
type RequestInfo struct {
|
||||
Method string `json:"method"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
RequestBody string `json:"request_body"`
|
||||
}
|
||||
|
||||
// ErrorHandler 错误处理器
|
||||
type ErrorHandler struct {
|
||||
errors map[string]*ErrorInfo
|
||||
mu sync.RWMutex
|
||||
maxErrors int
|
||||
retention time.Duration
|
||||
}
|
||||
|
||||
// NewErrorHandler 创建新的错误处理器
|
||||
func NewErrorHandler(maxErrors int, retention time.Duration) *ErrorHandler {
|
||||
eh := &ErrorHandler{
|
||||
errors: make(map[string]*ErrorInfo),
|
||||
maxErrors: maxErrors,
|
||||
retention: retention,
|
||||
}
|
||||
|
||||
// 启动错误清理协程
|
||||
go eh.cleanupRoutine()
|
||||
|
||||
return eh
|
||||
}
|
||||
|
||||
// RecoverMiddleware panic恢复中间件
|
||||
func (eh *ErrorHandler) RecoverMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// 记录错误信息
|
||||
stackTrace := getStackTrace()
|
||||
|
||||
errorInfo := &ErrorInfo{
|
||||
ID: fmt.Sprintf("panic_%d", time.Now().UnixNano()),
|
||||
Timestamp: time.Now(),
|
||||
Message: fmt.Sprintf("%v", err),
|
||||
StackTrace: stackTrace,
|
||||
RequestInfo: &RequestInfo{
|
||||
Method: c.Request.Method,
|
||||
URL: c.Request.URL.String(),
|
||||
RemoteAddr: c.ClientIP(),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
},
|
||||
Level: "error",
|
||||
Count: 1,
|
||||
}
|
||||
|
||||
// 保存错误信息
|
||||
eh.saveError(errorInfo)
|
||||
|
||||
utils.Error("Panic recovered: %v\nStack trace: %s", err, stackTrace)
|
||||
|
||||
// 返回错误响应
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Internal server error",
|
||||
"code": "INTERNAL_ERROR",
|
||||
})
|
||||
|
||||
// 不继续处理
|
||||
c.Abort()
|
||||
}
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorMiddleware 通用错误处理中间件
|
||||
func (eh *ErrorHandler) ErrorMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
|
||||
// 检查是否有错误
|
||||
if len(c.Errors) > 0 {
|
||||
for _, ginErr := range c.Errors {
|
||||
errorInfo := &ErrorInfo{
|
||||
ID: fmt.Sprintf("error_%d_%s", time.Now().UnixNano(), ginErr.Type),
|
||||
Timestamp: time.Now(),
|
||||
Message: ginErr.Error(),
|
||||
Level: "error",
|
||||
Count: 1,
|
||||
RequestInfo: &RequestInfo{
|
||||
Method: c.Request.Method,
|
||||
URL: c.Request.URL.String(),
|
||||
RemoteAddr: c.ClientIP(),
|
||||
UserAgent: c.GetHeader("User-Agent"),
|
||||
},
|
||||
}
|
||||
|
||||
eh.saveError(errorInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveError 保存错误信息
|
||||
func (eh *ErrorHandler) saveError(errorInfo *ErrorInfo) {
|
||||
eh.mu.Lock()
|
||||
defer eh.mu.Unlock()
|
||||
|
||||
key := errorInfo.Message
|
||||
if existing, exists := eh.errors[key]; exists {
|
||||
// 如果错误已存在,增加计数
|
||||
existing.Count++
|
||||
existing.Timestamp = time.Now()
|
||||
} else {
|
||||
// 如果是新错误,添加到映射中
|
||||
eh.errors[key] = errorInfo
|
||||
}
|
||||
|
||||
// 如果错误数量超过限制,清理旧错误
|
||||
if len(eh.errors) > eh.maxErrors {
|
||||
eh.cleanupOldErrors()
|
||||
}
|
||||
}
|
||||
|
||||
// GetErrors 获取错误列表
|
||||
func (eh *ErrorHandler) GetErrors() []*ErrorInfo {
|
||||
eh.mu.RLock()
|
||||
defer eh.mu.RUnlock()
|
||||
|
||||
errors := make([]*ErrorInfo, 0, len(eh.errors))
|
||||
for _, errorInfo := range eh.errors {
|
||||
errors = append(errors, errorInfo)
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// GetErrorByID 根据ID获取错误
|
||||
func (eh *ErrorHandler) GetErrorByID(id string) (*ErrorInfo, bool) {
|
||||
eh.mu.RLock()
|
||||
defer eh.mu.RUnlock()
|
||||
|
||||
for _, errorInfo := range eh.errors {
|
||||
if errorInfo.ID == id {
|
||||
return errorInfo, true
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ClearErrors 清空所有错误
|
||||
func (eh *ErrorHandler) ClearErrors() {
|
||||
eh.mu.Lock()
|
||||
defer eh.mu.Unlock()
|
||||
|
||||
eh.errors = make(map[string]*ErrorInfo)
|
||||
}
|
||||
|
||||
// cleanupOldErrors 清理旧错误
|
||||
func (eh *ErrorHandler) cleanupOldErrors() {
|
||||
// 简单策略:保留最近的错误,删除旧的
|
||||
errors := make([]*ErrorInfo, 0, len(eh.errors))
|
||||
for _, errorInfo := range eh.errors {
|
||||
errors = append(errors, errorInfo)
|
||||
}
|
||||
|
||||
// 按时间戳排序
|
||||
for i := 0; i < len(errors)-1; i++ {
|
||||
for j := i + 1; j < len(errors); j++ {
|
||||
if errors[i].Timestamp.Before(errors[j].Timestamp) {
|
||||
errors[i], errors[j] = errors[j], errors[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保留最新的maxErrors/2个错误
|
||||
keep := eh.maxErrors / 2
|
||||
if keep < 1 {
|
||||
keep = 1
|
||||
}
|
||||
|
||||
if len(errors) > keep {
|
||||
// 重建错误映射
|
||||
newErrors := make(map[string]*ErrorInfo)
|
||||
for i := 0; i < keep; i++ {
|
||||
newErrors[errors[i].Message] = errors[i]
|
||||
}
|
||||
eh.errors = newErrors
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupRoutine 定期清理过期错误的协程
|
||||
func (eh *ErrorHandler) cleanupRoutine() {
|
||||
ticker := time.NewTicker(5 * time.Minute) // 每5分钟清理一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
eh.mu.Lock()
|
||||
for key, errorInfo := range eh.errors {
|
||||
if time.Since(errorInfo.Timestamp) > eh.retention {
|
||||
delete(eh.errors, key)
|
||||
}
|
||||
}
|
||||
eh.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// getStackTrace 获取堆栈跟踪信息
|
||||
func getStackTrace() string {
|
||||
var buf [4096]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// GetErrorStatistics 获取错误统计信息
|
||||
func (eh *ErrorHandler) GetErrorStatistics() map[string]interface{} {
|
||||
eh.mu.RLock()
|
||||
defer eh.mu.RUnlock()
|
||||
|
||||
totalErrors := len(eh.errors)
|
||||
totalCount := 0
|
||||
errorTypes := make(map[string]int)
|
||||
|
||||
for _, errorInfo := range eh.errors {
|
||||
totalCount += errorInfo.Count
|
||||
// 提取错误类型(基于错误消息的前几个单词)
|
||||
parts := strings.Split(errorInfo.Message, " ")
|
||||
if len(parts) > 0 {
|
||||
errorType := strings.Join(parts[:min(3, len(parts))], " ")
|
||||
errorTypes[errorType]++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_errors": totalErrors,
|
||||
"total_count": totalCount,
|
||||
"error_types": errorTypes,
|
||||
"max_errors": eh.maxErrors,
|
||||
"retention": eh.retention,
|
||||
"active_errors": len(eh.errors),
|
||||
}
|
||||
}
|
||||
|
||||
// min 辅助函数
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// GlobalErrorHandler 全局错误处理器
|
||||
var globalErrorHandler *ErrorHandler
|
||||
|
||||
// InitGlobalErrorHandler 初始化全局错误处理器
|
||||
func InitGlobalErrorHandler(maxErrors int, retention time.Duration) {
|
||||
globalErrorHandler = NewErrorHandler(maxErrors, retention)
|
||||
}
|
||||
|
||||
// GetGlobalErrorHandler 获取全局错误处理器
|
||||
func GetGlobalErrorHandler() *ErrorHandler {
|
||||
if globalErrorHandler == nil {
|
||||
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||
}
|
||||
return globalErrorHandler
|
||||
}
|
||||
|
||||
// Recover 全局panic恢复函数
|
||||
func Recover() gin.HandlerFunc {
|
||||
if globalErrorHandler == nil {
|
||||
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||
}
|
||||
return globalErrorHandler.RecoverMiddleware()
|
||||
}
|
||||
|
||||
// Error 全局错误处理函数
|
||||
func Error() gin.HandlerFunc {
|
||||
if globalErrorHandler == nil {
|
||||
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||
}
|
||||
return globalErrorHandler.ErrorMiddleware()
|
||||
}
|
||||
|
||||
// RecordError 记录错误(全局函数)
|
||||
func RecordError(message string, level string) {
|
||||
if globalErrorHandler == nil {
|
||||
InitGlobalErrorHandler(100, 24*time.Hour)
|
||||
return
|
||||
}
|
||||
|
||||
errorInfo := &ErrorInfo{
|
||||
ID: fmt.Sprintf("%s_%d", level, time.Now().UnixNano()),
|
||||
Timestamp: time.Now(),
|
||||
Message: message,
|
||||
Level: level,
|
||||
Count: 1,
|
||||
}
|
||||
|
||||
globalErrorHandler.saveError(errorInfo)
|
||||
}
|
||||
458
monitor/metrics.go
Normal file
458
monitor/metrics.go
Normal file
@@ -0,0 +1,458 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Metrics 监控指标
|
||||
type Metrics struct {
|
||||
// HTTP请求指标
|
||||
RequestsTotal *prometheus.CounterVec
|
||||
RequestDuration *prometheus.HistogramVec
|
||||
RequestSize *prometheus.SummaryVec
|
||||
ResponseSize *prometheus.SummaryVec
|
||||
|
||||
// 数据库指标
|
||||
DatabaseQueries *prometheus.CounterVec
|
||||
DatabaseErrors *prometheus.CounterVec
|
||||
DatabaseDuration *prometheus.HistogramVec
|
||||
|
||||
// 系统指标
|
||||
MemoryUsage prometheus.Gauge
|
||||
Goroutines prometheus.Gauge
|
||||
GCStats *prometheus.CounterVec
|
||||
|
||||
// 业务指标
|
||||
ResourcesCreated *prometheus.CounterVec
|
||||
ResourcesViewed *prometheus.CounterVec
|
||||
Searches *prometheus.CounterVec
|
||||
Transfers *prometheus.CounterVec
|
||||
|
||||
// 错误指标
|
||||
ErrorsTotal *prometheus.CounterVec
|
||||
|
||||
// 自定义指标
|
||||
CustomCounters map[string]prometheus.Counter
|
||||
CustomGauges map[string]prometheus.Gauge
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// MetricsConfig 监控配置
|
||||
type MetricsConfig struct {
|
||||
Enabled bool
|
||||
ListenAddress string
|
||||
MetricsPath string
|
||||
Namespace string
|
||||
Subsystem string
|
||||
}
|
||||
|
||||
// DefaultMetricsConfig 默认监控配置
|
||||
func DefaultMetricsConfig() *MetricsConfig {
|
||||
return &MetricsConfig{
|
||||
Enabled: true,
|
||||
ListenAddress: ":9090",
|
||||
MetricsPath: "/metrics",
|
||||
Namespace: "urldb",
|
||||
Subsystem: "api",
|
||||
}
|
||||
}
|
||||
|
||||
// GlobalMetrics 全局监控实例
|
||||
var (
|
||||
globalMetrics *Metrics
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// NewMetrics 创建新的监控指标
|
||||
func NewMetrics(config *MetricsConfig) *Metrics {
|
||||
if config == nil {
|
||||
config = DefaultMetricsConfig()
|
||||
}
|
||||
|
||||
namespace := config.Namespace
|
||||
subsystem := config.Subsystem
|
||||
|
||||
m := &Metrics{
|
||||
// HTTP请求指标
|
||||
RequestsTotal: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "http_requests_total",
|
||||
Help: "Total number of HTTP requests",
|
||||
},
|
||||
[]string{"method", "endpoint", "status"},
|
||||
),
|
||||
RequestDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "http_request_duration_seconds",
|
||||
Help: "HTTP request duration in seconds",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
[]string{"method", "endpoint", "status"},
|
||||
),
|
||||
RequestSize: promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "http_request_size_bytes",
|
||||
Help: "HTTP request size in bytes",
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||
},
|
||||
[]string{"method", "endpoint"},
|
||||
),
|
||||
ResponseSize: promauto.NewSummaryVec(
|
||||
prometheus.SummaryOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "http_response_size_bytes",
|
||||
Help: "HTTP response size in bytes",
|
||||
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
|
||||
},
|
||||
[]string{"method", "endpoint"},
|
||||
),
|
||||
|
||||
// 数据库指标
|
||||
DatabaseQueries: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "database",
|
||||
Name: "queries_total",
|
||||
Help: "Total number of database queries",
|
||||
},
|
||||
[]string{"table", "operation"},
|
||||
),
|
||||
DatabaseErrors: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "database",
|
||||
Name: "errors_total",
|
||||
Help: "Total number of database errors",
|
||||
},
|
||||
[]string{"table", "operation", "error"},
|
||||
),
|
||||
DatabaseDuration: promauto.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "database",
|
||||
Name: "query_duration_seconds",
|
||||
Help: "Database query duration in seconds",
|
||||
Buckets: prometheus.DefBuckets,
|
||||
},
|
||||
[]string{"table", "operation"},
|
||||
),
|
||||
|
||||
// 系统指标
|
||||
MemoryUsage: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "system",
|
||||
Name: "memory_usage_bytes",
|
||||
Help: "Current memory usage in bytes",
|
||||
},
|
||||
),
|
||||
Goroutines: promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "system",
|
||||
Name: "goroutines",
|
||||
Help: "Number of goroutines",
|
||||
},
|
||||
),
|
||||
GCStats: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "system",
|
||||
Name: "gc_stats_total",
|
||||
Help: "Garbage collection statistics",
|
||||
},
|
||||
[]string{"type"},
|
||||
),
|
||||
|
||||
// 业务指标
|
||||
ResourcesCreated: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "business",
|
||||
Name: "resources_created_total",
|
||||
Help: "Total number of resources created",
|
||||
},
|
||||
[]string{"category", "platform"},
|
||||
),
|
||||
ResourcesViewed: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "business",
|
||||
Name: "resources_viewed_total",
|
||||
Help: "Total number of resources viewed",
|
||||
},
|
||||
[]string{"category"},
|
||||
),
|
||||
Searches: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "business",
|
||||
Name: "searches_total",
|
||||
Help: "Total number of searches",
|
||||
},
|
||||
[]string{"platform"},
|
||||
),
|
||||
Transfers: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "business",
|
||||
Name: "transfers_total",
|
||||
Help: "Total number of transfers",
|
||||
},
|
||||
[]string{"platform", "status"},
|
||||
),
|
||||
|
||||
// 错误指标
|
||||
ErrorsTotal: promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Subsystem: "errors",
|
||||
Name: "total",
|
||||
Help: "Total number of errors",
|
||||
},
|
||||
[]string{"type", "endpoint"},
|
||||
),
|
||||
|
||||
// 自定义指标
|
||||
CustomCounters: make(map[string]prometheus.Counter),
|
||||
CustomGauges: make(map[string]prometheus.Gauge),
|
||||
}
|
||||
|
||||
// 启动系统指标收集
|
||||
go m.collectSystemMetrics()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// GetGlobalMetrics 获取全局监控实例
|
||||
func GetGlobalMetrics() *Metrics {
|
||||
once.Do(func() {
|
||||
globalMetrics = NewMetrics(DefaultMetricsConfig())
|
||||
})
|
||||
return globalMetrics
|
||||
}
|
||||
|
||||
// SetGlobalMetrics 设置全局监控实例
|
||||
func SetGlobalMetrics(metrics *Metrics) {
|
||||
globalMetrics = metrics
|
||||
}
|
||||
|
||||
// collectSystemMetrics 收集系统指标
|
||||
func (m *Metrics) collectSystemMetrics() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
// 收集内存使用情况
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
m.MemoryUsage.Set(float64(ms.Alloc))
|
||||
|
||||
// 收集goroutine数量
|
||||
m.Goroutines.Set(float64(runtime.NumGoroutine()))
|
||||
|
||||
// 收集GC统计
|
||||
m.GCStats.WithLabelValues("alloc").Add(float64(ms.TotalAlloc))
|
||||
m.GCStats.WithLabelValues("sys").Add(float64(ms.Sys))
|
||||
m.GCStats.WithLabelValues("lookups").Add(float64(ms.Lookups))
|
||||
m.GCStats.WithLabelValues("mallocs").Add(float64(ms.Mallocs))
|
||||
m.GCStats.WithLabelValues("frees").Add(float64(ms.Frees))
|
||||
}
|
||||
}
|
||||
|
||||
// MetricsMiddleware 监控中间件
|
||||
func (m *Metrics) MetricsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.FullPath()
|
||||
|
||||
// 如果没有匹配的路由,使用请求路径
|
||||
if path == "" {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
|
||||
// 记录请求大小
|
||||
requestSize := float64(c.Request.ContentLength)
|
||||
m.RequestSize.WithLabelValues(c.Request.Method, path).Observe(requestSize)
|
||||
|
||||
c.Next()
|
||||
|
||||
// 记录响应信息
|
||||
status := c.Writer.Status()
|
||||
latency := time.Since(start).Seconds()
|
||||
responseSize := float64(c.Writer.Size())
|
||||
|
||||
// 更新指标
|
||||
m.RequestsTotal.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%d", status)).Inc()
|
||||
m.RequestDuration.WithLabelValues(c.Request.Method, path, fmt.Sprintf("%d", status)).Observe(latency)
|
||||
m.ResponseSize.WithLabelValues(c.Request.Method, path).Observe(responseSize)
|
||||
|
||||
// 如果是错误状态码,记录错误
|
||||
if status >= 400 {
|
||||
m.ErrorsTotal.WithLabelValues("http", path).Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StartMetricsServer 启动监控服务器
|
||||
func (m *Metrics) StartMetricsServer(config *MetricsConfig) {
|
||||
if config == nil {
|
||||
config = DefaultMetricsConfig()
|
||||
}
|
||||
|
||||
if !config.Enabled {
|
||||
utils.Info("监控服务器未启用")
|
||||
return
|
||||
}
|
||||
|
||||
// 创建新的Gin路由器
|
||||
router := gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
// 注册Prometheus指标端点
|
||||
router.GET(config.MetricsPath, gin.WrapH(promhttp.Handler()))
|
||||
|
||||
// 启动HTTP服务器
|
||||
go func() {
|
||||
utils.Info("监控服务器启动在 %s", config.ListenAddress)
|
||||
if err := router.Run(config.ListenAddress); err != nil {
|
||||
utils.Error("监控服务器启动失败: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
utils.Info("监控服务器已启动,指标路径: %s%s", config.ListenAddress, config.MetricsPath)
|
||||
}
|
||||
|
||||
// IncrementDatabaseQuery 增加数据库查询计数
|
||||
func (m *Metrics) IncrementDatabaseQuery(table, operation string) {
|
||||
m.DatabaseQueries.WithLabelValues(table, operation).Inc()
|
||||
}
|
||||
|
||||
// IncrementDatabaseError 增加数据库错误计数
|
||||
func (m *Metrics) IncrementDatabaseError(table, operation, error string) {
|
||||
m.DatabaseErrors.WithLabelValues(table, operation, error).Inc()
|
||||
}
|
||||
|
||||
// ObserveDatabaseDuration 记录数据库查询耗时
|
||||
func (m *Metrics) ObserveDatabaseDuration(table, operation string, duration float64) {
|
||||
m.DatabaseDuration.WithLabelValues(table, operation).Observe(duration)
|
||||
}
|
||||
|
||||
// IncrementResourceCreated 增加资源创建计数
|
||||
func (m *Metrics) IncrementResourceCreated(category, platform string) {
|
||||
m.ResourcesCreated.WithLabelValues(category, platform).Inc()
|
||||
}
|
||||
|
||||
// IncrementResourceViewed 增加资源查看计数
|
||||
func (m *Metrics) IncrementResourceViewed(category string) {
|
||||
m.ResourcesViewed.WithLabelValues(category).Inc()
|
||||
}
|
||||
|
||||
// IncrementSearch 增加搜索计数
|
||||
func (m *Metrics) IncrementSearch(platform string) {
|
||||
m.Searches.WithLabelValues(platform).Inc()
|
||||
}
|
||||
|
||||
// IncrementTransfer 增加转存计数
|
||||
func (m *Metrics) IncrementTransfer(platform, status string) {
|
||||
m.Transfers.WithLabelValues(platform, status).Inc()
|
||||
}
|
||||
|
||||
// IncrementError 增加错误计数
|
||||
func (m *Metrics) IncrementError(errorType, endpoint string) {
|
||||
m.ErrorsTotal.WithLabelValues(errorType, endpoint).Inc()
|
||||
}
|
||||
|
||||
// AddCustomCounter 添加自定义计数器
|
||||
func (m *Metrics) AddCustomCounter(name, help string, labels []string) prometheus.Counter {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s_%v", name, labels)
|
||||
if counter, exists := m.CustomCounters[key]; exists {
|
||||
return counter
|
||||
}
|
||||
|
||||
counter := promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: "urldb",
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
labels,
|
||||
).WithLabelValues() // 如果没有标签,返回默认实例
|
||||
|
||||
m.CustomCounters[key] = counter
|
||||
return counter
|
||||
}
|
||||
|
||||
// AddCustomGauge 添加自定义仪表盘
|
||||
func (m *Metrics) AddCustomGauge(name, help string, labels []string) prometheus.Gauge {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := fmt.Sprintf("%s_%v", name, labels)
|
||||
if gauge, exists := m.CustomGauges[key]; exists {
|
||||
return gauge
|
||||
}
|
||||
|
||||
gauge := promauto.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: "urldb",
|
||||
Name: name,
|
||||
Help: help,
|
||||
},
|
||||
labels,
|
||||
).WithLabelValues() // 如果没有标签,返回默认实例
|
||||
|
||||
m.CustomGauges[key] = gauge
|
||||
return gauge
|
||||
}
|
||||
|
||||
// GetMetricsSummary 获取指标摘要
|
||||
func (m *Metrics) GetMetricsSummary() map[string]interface{} {
|
||||
// 这里可以实现获取当前指标摘要的逻辑
|
||||
// 由于Prometheus指标不能直接读取,我们只能返回一些基本的统计信息
|
||||
return map[string]interface{}{
|
||||
"timestamp": time.Now(),
|
||||
"status": "running",
|
||||
"info": "使用 /metrics 端点获取详细指标",
|
||||
}
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (m *Metrics) HealthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().Unix(),
|
||||
"version": "1.0.0",
|
||||
})
|
||||
}
|
||||
|
||||
// SetupHealthCheck 设置健康检查端点
|
||||
func (m *Metrics) SetupHealthCheck(router *gin.Engine) {
|
||||
router.GET("/health", m.HealthCheck)
|
||||
router.GET("/healthz", m.HealthCheck)
|
||||
}
|
||||
|
||||
// MetricsHandler 指标处理器
|
||||
func (m *Metrics) MetricsHandler() gin.HandlerFunc {
|
||||
return gin.WrapH(promhttp.Handler())
|
||||
}
|
||||
25
monitor/setup.go
Normal file
25
monitor/setup.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SetupMonitoring 设置完整的监控系统
|
||||
func SetupMonitoring(router *gin.Engine) {
|
||||
// 获取全局监控实例
|
||||
metrics := GetGlobalMetrics()
|
||||
|
||||
// 设置健康检查端点
|
||||
metrics.SetupHealthCheck(router)
|
||||
|
||||
// 设置指标端点
|
||||
router.GET("/metrics", metrics.MetricsHandler())
|
||||
|
||||
utils.Info("监控系统已设置完成")
|
||||
}
|
||||
|
||||
// SetGlobalErrorHandler 设置全局错误处理器
|
||||
func SetGlobalErrorHandler(eh *ErrorHandler) {
|
||||
globalErrorHandler = eh
|
||||
}
|
||||
@@ -60,17 +60,38 @@ server {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
|
||||
# 缓存设置
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
|
||||
|
||||
# 允许跨域访问
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept";
|
||||
}
|
||||
|
||||
# 微信公众号验证文件路由 - 根目录的TXT文件直接访问后端uploads目录
|
||||
location ~ ^/[^/]+\.txt$ {
|
||||
# 检查文件是否存在于uploads目录
|
||||
set $uploads_path /uploads$uri;
|
||||
if (-f $uploads_path) {
|
||||
proxy_pass http://backend;
|
||||
|
||||
# 缓存设置
|
||||
expires 1h;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
# 允许跨域访问
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS";
|
||||
add_header Access-Control-Allow-Headers "Origin, Content-Type, Accept";
|
||||
break;
|
||||
}
|
||||
# 如果文件不存在,返回404
|
||||
return 404;
|
||||
}
|
||||
|
||||
# 健康检查
|
||||
location /health {
|
||||
proxy_pass http://backend/health;
|
||||
|
||||
119
plugin/cache/cache.go
vendored
Normal file
119
plugin/cache/cache.go
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// CacheItem 缓存项
|
||||
type CacheItem struct {
|
||||
Value interface{}
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// CacheManager 缓存管理器
|
||||
type CacheManager struct {
|
||||
cache map[string]*CacheItem
|
||||
mutex sync.RWMutex
|
||||
defaultTTL time.Duration
|
||||
}
|
||||
|
||||
// NewCacheManager 创建新的缓存管理器
|
||||
func NewCacheManager(defaultTTL time.Duration) *CacheManager {
|
||||
cm := &CacheManager{
|
||||
cache: make(map[string]*CacheItem),
|
||||
defaultTTL: defaultTTL,
|
||||
}
|
||||
|
||||
// 启动定期清理过期缓存的goroutine
|
||||
go cm.cleanupExpired()
|
||||
|
||||
return cm
|
||||
}
|
||||
|
||||
// Set 设置缓存项
|
||||
func (cm *CacheManager) Set(key string, value interface{}, ttl time.Duration) {
|
||||
cm.mutex.Lock()
|
||||
defer cm.mutex.Unlock()
|
||||
|
||||
expiration := time.Now().Add(ttl)
|
||||
cm.cache[key] = &CacheItem{
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// Get 获取缓存项
|
||||
func (cm *CacheManager) Get(key string) (interface{}, bool) {
|
||||
cm.mutex.RLock()
|
||||
defer cm.mutex.RUnlock()
|
||||
|
||||
item, exists := cm.cache[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if time.Now().After(item.Expiration) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return item.Value, true
|
||||
}
|
||||
|
||||
// Delete 删除缓存项
|
||||
func (cm *CacheManager) Delete(key string) {
|
||||
cm.mutex.Lock()
|
||||
defer cm.mutex.Unlock()
|
||||
|
||||
delete(cm.cache, key)
|
||||
}
|
||||
|
||||
// Clear 清空所有缓存
|
||||
func (cm *CacheManager) Clear() {
|
||||
cm.mutex.Lock()
|
||||
defer cm.mutex.Unlock()
|
||||
|
||||
cm.cache = make(map[string]*CacheItem)
|
||||
}
|
||||
|
||||
// cleanupExpired 定期清理过期缓存
|
||||
func (cm *CacheManager) cleanupExpired() {
|
||||
ticker := time.NewTicker(5 * time.Minute) // 每5分钟清理一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
cm.mutex.Lock()
|
||||
now := time.Now()
|
||||
for key, item := range cm.cache {
|
||||
if now.After(item.Expiration) {
|
||||
delete(cm.cache, key)
|
||||
utils.Debug("Cleaned up expired cache item: %s", key)
|
||||
}
|
||||
}
|
||||
cm.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats 获取缓存统计信息
|
||||
func (cm *CacheManager) GetStats() map[string]interface{} {
|
||||
cm.mutex.RLock()
|
||||
defer cm.mutex.RUnlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats["total_items"] = len(cm.cache)
|
||||
|
||||
// 计算过期项数量
|
||||
now := time.Now()
|
||||
expiredCount := 0
|
||||
for _, item := range cm.cache {
|
||||
if now.After(item.Expiration) {
|
||||
expiredCount++
|
||||
}
|
||||
}
|
||||
stats["expired_items"] = expiredCount
|
||||
|
||||
return stats
|
||||
}
|
||||
299
plugin/concurrency/controller.go
Normal file
299
plugin/concurrency/controller.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package concurrency
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// ConcurrencyController 并发控制器
|
||||
type ConcurrencyController struct {
|
||||
// 每个插件的最大并发数
|
||||
pluginLimits map[string]int
|
||||
// 当前每个插件的活跃任务数
|
||||
pluginActiveTasks map[string]int
|
||||
// 全局最大并发数
|
||||
globalLimit int
|
||||
// 全局活跃任务数
|
||||
globalActiveTasks int
|
||||
// 等待队列
|
||||
waitingTasks map[string][]*WaitingTask
|
||||
// 互斥锁
|
||||
mutex sync.Mutex
|
||||
// 条件变量用于任务等待和通知
|
||||
cond *sync.Cond
|
||||
}
|
||||
|
||||
// WaitingTask 等待执行的任务
|
||||
type WaitingTask struct {
|
||||
PluginName string
|
||||
TaskFunc func() error
|
||||
Ctx context.Context
|
||||
Cancel context.CancelFunc
|
||||
ResultChan chan error
|
||||
}
|
||||
|
||||
// NewConcurrencyController 创建新的并发控制器
|
||||
func NewConcurrencyController(globalLimit int) *ConcurrencyController {
|
||||
cc := &ConcurrencyController{
|
||||
pluginLimits: make(map[string]int),
|
||||
pluginActiveTasks: make(map[string]int),
|
||||
globalLimit: globalLimit,
|
||||
waitingTasks: make(map[string][]*WaitingTask),
|
||||
}
|
||||
|
||||
cc.cond = sync.NewCond(&cc.mutex)
|
||||
return cc
|
||||
}
|
||||
|
||||
// SetPluginLimit 设置插件的并发限制
|
||||
func (cc *ConcurrencyController) SetPluginLimit(pluginName string, limit int) {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
cc.pluginLimits[pluginName] = limit
|
||||
utils.Info("Set concurrency limit for plugin %s to %d", pluginName, limit)
|
||||
}
|
||||
|
||||
// GetPluginLimit 获取插件的并发限制
|
||||
func (cc *ConcurrencyController) GetPluginLimit(pluginName string) int {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
return cc.pluginLimits[pluginName]
|
||||
}
|
||||
|
||||
// Execute 执行受并发控制的任务
|
||||
func (cc *ConcurrencyController) Execute(ctx context.Context, pluginName string, taskFunc func() error) error {
|
||||
// 创建结果通道
|
||||
resultChan := make(chan error, 1)
|
||||
|
||||
// 尝试获取执行许可
|
||||
permitted := cc.tryAcquire(pluginName)
|
||||
if !permitted {
|
||||
// 需要等待
|
||||
waitingTask := &WaitingTask{
|
||||
PluginName: pluginName,
|
||||
TaskFunc: taskFunc,
|
||||
Ctx: ctx,
|
||||
ResultChan: resultChan,
|
||||
}
|
||||
|
||||
// 添加到等待队列
|
||||
cc.mutex.Lock()
|
||||
cc.waitingTasks[pluginName] = append(cc.waitingTasks[pluginName], waitingTask)
|
||||
cc.mutex.Unlock()
|
||||
|
||||
utils.Debug("Task for plugin %s added to waiting queue", pluginName)
|
||||
|
||||
// 等待结果或上下文取消
|
||||
select {
|
||||
case err := <-resultChan:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
// 从等待队列中移除任务
|
||||
cc.removeFromWaitingQueue(pluginName, waitingTask)
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// 可以立即执行
|
||||
defer cc.release(pluginName)
|
||||
|
||||
// 在goroutine中执行任务,以支持超时和取消
|
||||
taskCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
taskResult := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(taskResult)
|
||||
taskResult <- taskFunc()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-taskResult:
|
||||
return err
|
||||
case <-taskCtx.Done():
|
||||
return taskCtx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// tryAcquire 尝试获取执行许可
|
||||
func (cc *ConcurrencyController) tryAcquire(pluginName string) bool {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
// 检查全局限制
|
||||
if cc.globalActiveTasks >= cc.globalLimit {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查插件限制
|
||||
pluginLimit := cc.pluginLimits[pluginName]
|
||||
if pluginLimit <= 0 {
|
||||
// 如果没有设置限制,使用默认值(全局限制的1/4,但至少为1)
|
||||
pluginLimit = cc.globalLimit / 4
|
||||
if pluginLimit < 1 {
|
||||
pluginLimit = 1
|
||||
}
|
||||
}
|
||||
|
||||
if cc.pluginActiveTasks[pluginName] >= pluginLimit {
|
||||
return false
|
||||
}
|
||||
|
||||
// 增加计数
|
||||
cc.globalActiveTasks++
|
||||
cc.pluginActiveTasks[pluginName]++
|
||||
return true
|
||||
}
|
||||
|
||||
// release 释放执行许可
|
||||
func (cc *ConcurrencyController) release(pluginName string) {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
// 减少计数
|
||||
cc.globalActiveTasks--
|
||||
if cc.globalActiveTasks < 0 {
|
||||
cc.globalActiveTasks = 0
|
||||
}
|
||||
|
||||
cc.pluginActiveTasks[pluginName]--
|
||||
if cc.pluginActiveTasks[pluginName] < 0 {
|
||||
cc.pluginActiveTasks[pluginName] = 0
|
||||
}
|
||||
|
||||
// 检查是否有等待的任务可以执行
|
||||
cc.checkWaitingTasks()
|
||||
|
||||
// 通知等待的goroutine
|
||||
cc.cond.Broadcast()
|
||||
}
|
||||
|
||||
// checkWaitingTasks 检查等待队列中的任务是否可以执行
|
||||
func (cc *ConcurrencyController) checkWaitingTasks() {
|
||||
for pluginName, tasks := range cc.waitingTasks {
|
||||
if len(tasks) > 0 {
|
||||
// 检查是否可以获得执行许可
|
||||
if cc.tryAcquire(pluginName) {
|
||||
// 获取第一个等待的任务
|
||||
task := tasks[0]
|
||||
cc.waitingTasks[pluginName] = tasks[1:]
|
||||
|
||||
// 在新的goroutine中执行任务
|
||||
go cc.executeWaitingTask(task, pluginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeWaitingTask 执行等待的任务
|
||||
func (cc *ConcurrencyController) executeWaitingTask(task *WaitingTask, pluginName string) {
|
||||
defer cc.release(pluginName)
|
||||
|
||||
// 检查上下文是否已取消
|
||||
select {
|
||||
case <-task.Ctx.Done():
|
||||
task.ResultChan <- task.Ctx.Err()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 执行任务
|
||||
err := task.TaskFunc()
|
||||
|
||||
// 发送结果
|
||||
select {
|
||||
case task.ResultChan <- err:
|
||||
default:
|
||||
// 结果通道已关闭或已满
|
||||
utils.Warn("Failed to send result for waiting task of plugin %s", pluginName)
|
||||
}
|
||||
}
|
||||
|
||||
// removeFromWaitingQueue 从等待队列中移除任务
|
||||
func (cc *ConcurrencyController) removeFromWaitingQueue(pluginName string, task *WaitingTask) {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
if tasks, exists := cc.waitingTasks[pluginName]; exists {
|
||||
for i, t := range tasks {
|
||||
if t == task {
|
||||
// 从队列中移除
|
||||
cc.waitingTasks[pluginName] = append(tasks[:i], tasks[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetStats 获取并发控制器的统计信息
|
||||
func (cc *ConcurrencyController) GetStats() map[string]interface{} {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
stats := make(map[string]interface{})
|
||||
stats["global_limit"] = cc.globalLimit
|
||||
stats["global_active"] = cc.globalActiveTasks
|
||||
|
||||
pluginStats := make(map[string]interface{})
|
||||
for pluginName, limit := range cc.pluginLimits {
|
||||
pluginStat := make(map[string]interface{})
|
||||
pluginStat["limit"] = limit
|
||||
pluginStat["active"] = cc.pluginActiveTasks[pluginName]
|
||||
if tasks, exists := cc.waitingTasks[pluginName]; exists {
|
||||
pluginStat["waiting"] = len(tasks)
|
||||
} else {
|
||||
pluginStat["waiting"] = 0
|
||||
}
|
||||
pluginStats[pluginName] = pluginStat
|
||||
}
|
||||
stats["plugins"] = pluginStats
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
// WaitForAvailable 等待直到有可用的并发槽位
|
||||
func (cc *ConcurrencyController) WaitForAvailable(ctx context.Context, pluginName string) error {
|
||||
cc.mutex.Lock()
|
||||
defer cc.mutex.Unlock()
|
||||
|
||||
for {
|
||||
// 检查是否有可用的槽位
|
||||
pluginLimit := cc.pluginLimits[pluginName]
|
||||
if pluginLimit <= 0 {
|
||||
// 如果没有设置限制,使用默认值
|
||||
pluginLimit = cc.globalLimit / 4
|
||||
if pluginLimit < 1 {
|
||||
pluginLimit = 1
|
||||
}
|
||||
}
|
||||
|
||||
if cc.globalActiveTasks < cc.globalLimit && cc.pluginActiveTasks[pluginName] < pluginLimit {
|
||||
return nil // 有可用槽位
|
||||
}
|
||||
|
||||
// 等待或超时
|
||||
waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 使用goroutine来处理条件等待
|
||||
waitDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(waitDone)
|
||||
cc.cond.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitDone:
|
||||
// 条件满足,继续检查
|
||||
case <-waitCtx.Done():
|
||||
// 超时或取消
|
||||
return fmt.Errorf("timeout waiting for available slot: %v", waitCtx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
155
plugin/config/config_test.go
Normal file
155
plugin/config/config_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigSchema(t *testing.T) {
|
||||
// 创建配置模式
|
||||
schema := NewConfigSchema("test-plugin", "1.0.0")
|
||||
|
||||
// 添加字段
|
||||
intervalMin := 1.0
|
||||
intervalMax := 3600.0
|
||||
schema.AddField(ConfigField{
|
||||
Key: "interval",
|
||||
Name: "检查间隔",
|
||||
Description: "插件执行任务的时间间隔(秒)",
|
||||
Type: "int",
|
||||
Required: true,
|
||||
Default: 60,
|
||||
Min: &intervalMin,
|
||||
Max: &intervalMax,
|
||||
})
|
||||
|
||||
schema.AddField(ConfigField{
|
||||
Key: "enabled",
|
||||
Name: "启用状态",
|
||||
Description: "插件是否启用",
|
||||
Type: "bool",
|
||||
Required: true,
|
||||
Default: true,
|
||||
})
|
||||
|
||||
// 验证配置
|
||||
config := map[string]interface{}{
|
||||
"interval": 30,
|
||||
"enabled": true,
|
||||
}
|
||||
|
||||
validator := NewConfigValidator(schema)
|
||||
if err := validator.Validate(config); err != nil {
|
||||
t.Errorf("配置验证失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试无效配置
|
||||
invalidConfig := map[string]interface{}{
|
||||
"interval": 5000, // 超出最大值
|
||||
"enabled": true,
|
||||
}
|
||||
|
||||
if err := validator.Validate(invalidConfig); err == nil {
|
||||
t.Error("应该验证失败,但没有失败")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplate(t *testing.T) {
|
||||
// 创建模板管理器
|
||||
manager := NewConfigTemplateManager()
|
||||
|
||||
// 创建模板
|
||||
config := map[string]interface{}{
|
||||
"interval": 30,
|
||||
"enabled": true,
|
||||
"protocol": "https",
|
||||
}
|
||||
|
||||
template := &ConfigTemplate{
|
||||
Name: "default-config",
|
||||
Description: "默认配置模板",
|
||||
Config: config,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
// 注册模板
|
||||
if err := manager.RegisterTemplate(template); err != nil {
|
||||
t.Errorf("注册模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取模板
|
||||
retrievedTemplate, err := manager.GetTemplate("default-config")
|
||||
if err != nil {
|
||||
t.Errorf("获取模板失败: %v", err)
|
||||
}
|
||||
|
||||
if retrievedTemplate.Name != "default-config" {
|
||||
t.Error("模板名称不匹配")
|
||||
}
|
||||
|
||||
// 应用模板到配置
|
||||
targetConfig := make(map[string]interface{})
|
||||
if err := manager.ApplyTemplate("default-config", targetConfig); err != nil {
|
||||
t.Errorf("应用模板失败: %v", err)
|
||||
}
|
||||
|
||||
if targetConfig["interval"] != 30 {
|
||||
t.Error("模板应用不正确")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigVersion(t *testing.T) {
|
||||
// 创建版本管理器
|
||||
manager := NewConfigVersionManager(3)
|
||||
|
||||
// 保存版本
|
||||
config1 := map[string]interface{}{
|
||||
"interval": 30,
|
||||
"enabled": true,
|
||||
}
|
||||
|
||||
if err := manager.SaveVersion("test-plugin", "1.0.0", "初始版本", "tester", config1); err != nil {
|
||||
t.Errorf("保存版本失败: %v", err)
|
||||
}
|
||||
|
||||
// 获取最新版本
|
||||
latest, err := manager.GetLatestVersion("test-plugin")
|
||||
if err != nil {
|
||||
t.Errorf("获取最新版本失败: %v", err)
|
||||
}
|
||||
|
||||
if latest.Version != "1.0.0" {
|
||||
t.Error("版本不匹配")
|
||||
}
|
||||
|
||||
// 保存更多版本以测试限制
|
||||
config2 := map[string]interface{}{
|
||||
"interval": 60,
|
||||
"enabled": true,
|
||||
}
|
||||
|
||||
config3 := map[string]interface{}{
|
||||
"interval": 90,
|
||||
"enabled": false,
|
||||
}
|
||||
|
||||
config4 := map[string]interface{}{
|
||||
"interval": 120,
|
||||
"enabled": true,
|
||||
}
|
||||
|
||||
manager.SaveVersion("test-plugin", "1.1.0", "第二版本", "tester", config2)
|
||||
manager.SaveVersion("test-plugin", "1.2.0", "第三版本", "tester", config3)
|
||||
manager.SaveVersion("test-plugin", "1.3.0", "第四版本", "tester", config4)
|
||||
|
||||
// 检查版本数量限制
|
||||
versions, _ := manager.ListVersions("test-plugin")
|
||||
if len(versions) != 3 {
|
||||
t.Errorf("版本数量不正确,期望3个,实际%d个", len(versions))
|
||||
}
|
||||
|
||||
// 最新版本应该是1.3.0
|
||||
latest, _ = manager.GetLatestVersion("test-plugin")
|
||||
if latest.Version != "1.3.0" {
|
||||
t.Error("最新版本不正确")
|
||||
}
|
||||
}
|
||||
119
plugin/config/example_usage.go
Normal file
119
plugin/config/example_usage.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// ExampleUsage 演示如何使用插件配置系统
|
||||
func ExampleUsage() {
|
||||
// 创建插件管理器
|
||||
manager := NewConfigManager()
|
||||
|
||||
// 1. 创建配置模式
|
||||
fmt.Println("1. 创建配置模式")
|
||||
schema := NewConfigSchema("example-plugin", "1.0.0")
|
||||
|
||||
// 添加配置字段
|
||||
intervalMin := 1.0
|
||||
intervalMax := 3600.0
|
||||
schema.AddField(ConfigField{
|
||||
Key: "interval",
|
||||
Name: "检查间隔",
|
||||
Description: "插件执行任务的时间间隔(秒)",
|
||||
Type: "int",
|
||||
Required: true,
|
||||
Default: 60,
|
||||
Min: &intervalMin,
|
||||
Max: &intervalMax,
|
||||
})
|
||||
|
||||
schema.AddField(ConfigField{
|
||||
Key: "enabled",
|
||||
Name: "启用状态",
|
||||
Description: "插件是否启用",
|
||||
Type: "bool",
|
||||
Required: true,
|
||||
Default: true,
|
||||
})
|
||||
|
||||
schema.AddField(ConfigField{
|
||||
Key: "api_key",
|
||||
Name: "API密钥",
|
||||
Description: "访问外部服务的API密钥",
|
||||
Type: "string",
|
||||
Required: false,
|
||||
Encrypted: true,
|
||||
})
|
||||
|
||||
// 注册模式
|
||||
if err := manager.RegisterSchema(schema); err != nil {
|
||||
log.Fatalf("注册模式失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 创建配置模板
|
||||
fmt.Println("2. 创建配置模板")
|
||||
config := map[string]interface{}{
|
||||
"interval": 30,
|
||||
"enabled": true,
|
||||
"protocol": "https",
|
||||
}
|
||||
|
||||
template := &ConfigTemplate{
|
||||
Name: "production-config",
|
||||
Description: "生产环境配置模板",
|
||||
Config: config,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
if err := manager.RegisterTemplate(template); err != nil {
|
||||
log.Fatalf("注册模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 3. 验证配置
|
||||
fmt.Println("3. 验证配置")
|
||||
userConfig := map[string]interface{}{
|
||||
"interval": 120,
|
||||
"enabled": true,
|
||||
"api_key": "secret-key-12345",
|
||||
}
|
||||
|
||||
if err := manager.ValidateConfig("example-plugin", userConfig); err != nil {
|
||||
log.Fatalf("配置验证失败: %v", err)
|
||||
} else {
|
||||
fmt.Println("配置验证通过")
|
||||
}
|
||||
|
||||
// 4. 保存配置版本
|
||||
fmt.Println("4. 保存配置版本")
|
||||
if err := manager.SaveVersion("example-plugin", "1.0.0", "初始生产配置", "admin", userConfig); err != nil {
|
||||
log.Fatalf("保存配置版本失败: %v", err)
|
||||
}
|
||||
|
||||
// 5. 应用模板
|
||||
fmt.Println("5. 应用模板")
|
||||
newConfig := make(map[string]interface{})
|
||||
if err := manager.ApplyTemplate("example-plugin", "production-config", newConfig); err != nil {
|
||||
log.Fatalf("应用模板失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("应用模板后的配置: %+v\n", newConfig)
|
||||
|
||||
// 6. 获取最新版本
|
||||
fmt.Println("6. 获取最新版本")
|
||||
latestConfig, err := manager.GetLatestVersion("example-plugin")
|
||||
if err != nil {
|
||||
log.Fatalf("获取最新版本失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("最新配置版本: %+v\n", latestConfig)
|
||||
|
||||
// 7. 列出所有模板
|
||||
fmt.Println("7. 列出所有模板")
|
||||
templates := manager.ListTemplates()
|
||||
for _, tmpl := range templates {
|
||||
fmt.Printf("模板: %s - %s\n", tmpl.Name, tmpl.Description)
|
||||
}
|
||||
|
||||
fmt.Println("配置系统演示完成")
|
||||
}
|
||||
154
plugin/config/manager.go
Normal file
154
plugin/config/manager.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ConfigManager 插件配置管理器
|
||||
type ConfigManager struct {
|
||||
schemas map[string]*ConfigSchema
|
||||
templates *ConfigTemplateManager
|
||||
versions *ConfigVersionManager
|
||||
validator *ConfigValidator
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewConfigManager 创建新的配置管理器
|
||||
func NewConfigManager() *ConfigManager {
|
||||
return &ConfigManager{
|
||||
schemas: make(map[string]*ConfigSchema),
|
||||
templates: NewConfigTemplateManager(),
|
||||
versions: NewConfigVersionManager(10),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterSchema 注册配置模式
|
||||
func (m *ConfigManager) RegisterSchema(schema *ConfigSchema) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
if schema.PluginName == "" {
|
||||
return fmt.Errorf("plugin name cannot be empty")
|
||||
}
|
||||
|
||||
m.schemas[schema.PluginName] = schema
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSchema 获取配置模式
|
||||
func (m *ConfigManager) GetSchema(pluginName string) (*ConfigSchema, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
schema, exists := m.schemas[pluginName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("schema not found for plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
// ValidateConfig 验证插件配置
|
||||
func (m *ConfigManager) ValidateConfig(pluginName string, config map[string]interface{}) error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
schema, exists := m.schemas[pluginName]
|
||||
if !exists {
|
||||
return fmt.Errorf("schema not found for plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
validator := NewConfigValidator(schema)
|
||||
return validator.Validate(config)
|
||||
}
|
||||
|
||||
// ApplyTemplate 应用配置模板
|
||||
func (m *ConfigManager) ApplyTemplate(pluginName, templateName string, config map[string]interface{}) error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return m.templates.ApplyTemplate(templateName, config)
|
||||
}
|
||||
|
||||
// SaveVersion 保存配置版本
|
||||
func (m *ConfigManager) SaveVersion(pluginName, version, description, author string, config map[string]interface{}) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
return m.versions.SaveVersion(pluginName, version, description, author, config)
|
||||
}
|
||||
|
||||
// GetLatestVersion 获取最新配置版本
|
||||
func (m *ConfigManager) GetLatestVersion(pluginName string) (map[string]interface{}, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
version, err := m.versions.GetLatestVersion(pluginName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回配置副本
|
||||
configCopy := make(map[string]interface{})
|
||||
for k, v := range version.Config {
|
||||
configCopy[k] = v
|
||||
}
|
||||
|
||||
return configCopy, nil
|
||||
}
|
||||
|
||||
// RevertToVersion 回滚到指定版本
|
||||
func (m *ConfigManager) RevertToVersion(pluginName, version string) (map[string]interface{}, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return m.versions.RevertToVersion(pluginName, version)
|
||||
}
|
||||
|
||||
// ListVersions 列出配置版本
|
||||
func (m *ConfigManager) ListVersions(pluginName string) ([]*ConfigVersion, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return m.versions.ListVersions(pluginName)
|
||||
}
|
||||
|
||||
// RegisterTemplate 注册配置模板
|
||||
func (m *ConfigManager) RegisterTemplate(template *ConfigTemplate) error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
return m.templates.RegisterTemplate(template)
|
||||
}
|
||||
|
||||
// GetTemplate 获取配置模板
|
||||
func (m *ConfigManager) GetTemplate(name string) (*ConfigTemplate, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return m.templates.GetTemplate(name)
|
||||
}
|
||||
|
||||
// ListTemplates 列出所有模板
|
||||
func (m *ConfigManager) ListTemplates() []*ConfigTemplate {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
return m.templates.ListTemplates()
|
||||
}
|
||||
|
||||
// ApplyDefaults 应用默认值
|
||||
func (m *ConfigManager) ApplyDefaults(pluginName string, config map[string]interface{}) error {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
schema, exists := m.schemas[pluginName]
|
||||
if !exists {
|
||||
return fmt.Errorf("schema not found for plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
validator := NewConfigValidator(schema)
|
||||
validator.ApplyDefaults(config)
|
||||
return nil
|
||||
}
|
||||
164
plugin/config/schema.go
Normal file
164
plugin/config/schema.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConfigField 定义配置字段的结构
|
||||
type ConfigField struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"` // string, int, bool, float, json
|
||||
Required bool `json:"required"`
|
||||
Default interface{} `json:"default,omitempty"`
|
||||
Min *float64 `json:"min,omitempty"`
|
||||
Max *float64 `json:"max,omitempty"`
|
||||
Enum []string `json:"enum,omitempty"`
|
||||
Pattern string `json:"pattern,omitempty"`
|
||||
Encrypted bool `json:"encrypted,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigSchema 定义插件配置模式
|
||||
type ConfigSchema struct {
|
||||
PluginName string `json:"plugin_name"`
|
||||
Version string `json:"version"`
|
||||
Fields []ConfigField `json:"fields"`
|
||||
}
|
||||
|
||||
// NewConfigSchema 创建新的配置模式
|
||||
func NewConfigSchema(pluginName, version string) *ConfigSchema {
|
||||
return &ConfigSchema{
|
||||
PluginName: pluginName,
|
||||
Version: version,
|
||||
Fields: make([]ConfigField, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// AddField 添加配置字段
|
||||
func (s *ConfigSchema) AddField(field ConfigField) {
|
||||
s.Fields = append(s.Fields, field)
|
||||
}
|
||||
|
||||
// GetField 获取配置字段
|
||||
func (s *ConfigSchema) GetField(key string) (*ConfigField, bool) {
|
||||
for i := range s.Fields {
|
||||
if s.Fields[i].Key == key {
|
||||
return &s.Fields[i], true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ToJSON 将配置模式转换为JSON
|
||||
func (s *ConfigSchema) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON创建配置模式
|
||||
func (s *ConfigSchema) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, s)
|
||||
}
|
||||
|
||||
// Validate 验证配置是否符合模式
|
||||
func (s *ConfigSchema) Validate(config map[string]interface{}) error {
|
||||
// 验证必需字段
|
||||
for _, field := range s.Fields {
|
||||
if field.Required {
|
||||
if _, exists := config[field.Key]; !exists {
|
||||
// 检查是否有默认值
|
||||
if field.Default == nil {
|
||||
return fmt.Errorf("required field '%s' is missing", field.Key)
|
||||
}
|
||||
// 设置默认值
|
||||
config[field.Key] = field.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证字段类型和值
|
||||
for key, value := range config {
|
||||
field, exists := s.GetField(key)
|
||||
if !exists {
|
||||
// 允许额外字段存在,但不验证
|
||||
continue
|
||||
}
|
||||
|
||||
if err := s.validateField(field, value); err != nil {
|
||||
return fmt.Errorf("field '%s': %v", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateField 验证单个字段
|
||||
func (s *ConfigSchema) validateField(field *ConfigField, value interface{}) error {
|
||||
switch field.Type {
|
||||
case "string":
|
||||
if str, ok := value.(string); ok {
|
||||
if field.Pattern != "" {
|
||||
// 这里可以添加正则表达式验证
|
||||
}
|
||||
if field.Enum != nil && len(field.Enum) > 0 {
|
||||
found := false
|
||||
for _, enumValue := range field.Enum {
|
||||
if str == enumValue {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("value '%s' is not in enum %v", str, field.Enum)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("expected string, got %T", value)
|
||||
}
|
||||
case "int":
|
||||
if num, ok := value.(int); ok {
|
||||
if field.Min != nil && float64(num) < *field.Min {
|
||||
return fmt.Errorf("value %d is less than minimum %f", num, *field.Min)
|
||||
}
|
||||
if field.Max != nil && float64(num) > *field.Max {
|
||||
return fmt.Errorf("value %d is greater than maximum %f", num, *field.Max)
|
||||
}
|
||||
} else if num, ok := value.(float64); ok {
|
||||
// 允许float64转换为int
|
||||
intVal := int(num)
|
||||
if field.Min != nil && float64(intVal) < *field.Min {
|
||||
return fmt.Errorf("value %d is less than minimum %f", intVal, *field.Min)
|
||||
}
|
||||
if field.Max != nil && float64(intVal) > *field.Max {
|
||||
return fmt.Errorf("value %d is greater than maximum %f", intVal, *field.Max)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("expected integer, got %T", value)
|
||||
}
|
||||
case "bool":
|
||||
if _, ok := value.(bool); !ok {
|
||||
return fmt.Errorf("expected boolean, got %T", value)
|
||||
}
|
||||
case "float":
|
||||
if num, ok := value.(float64); ok {
|
||||
if field.Min != nil && num < *field.Min {
|
||||
return fmt.Errorf("value %f is less than minimum %f", num, *field.Min)
|
||||
}
|
||||
if field.Max != nil && num > *field.Max {
|
||||
return fmt.Errorf("value %f is greater than maximum %f", num, *field.Max)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("expected float, got %T", value)
|
||||
}
|
||||
case "json":
|
||||
// JSON类型可以是任何结构,只需要能序列化为JSON
|
||||
if _, err := json.Marshal(value); err != nil {
|
||||
return fmt.Errorf("invalid JSON value: %v", err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported field type: %s", field.Type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
93
plugin/config/template.go
Normal file
93
plugin/config/template.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConfigTemplate 配置模板
|
||||
type ConfigTemplate struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
SchemaRef string `json:"schema_ref,omitempty"` // 引用的模式ID
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// ConfigTemplateManager 配置模板管理器
|
||||
type ConfigTemplateManager struct {
|
||||
templates map[string]*ConfigTemplate
|
||||
}
|
||||
|
||||
// NewConfigTemplateManager 创建新的配置模板管理器
|
||||
func NewConfigTemplateManager() *ConfigTemplateManager {
|
||||
return &ConfigTemplateManager{
|
||||
templates: make(map[string]*ConfigTemplate),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterTemplate 注册配置模板
|
||||
func (m *ConfigTemplateManager) RegisterTemplate(template *ConfigTemplate) error {
|
||||
if template.Name == "" {
|
||||
return fmt.Errorf("template name cannot be empty")
|
||||
}
|
||||
|
||||
m.templates[template.Name] = template
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTemplate 获取配置模板
|
||||
func (m *ConfigTemplateManager) GetTemplate(name string) (*ConfigTemplate, error) {
|
||||
template, exists := m.templates[name]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("template '%s' not found", name)
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// ListTemplates 列出所有模板
|
||||
func (m *ConfigTemplateManager) ListTemplates() []*ConfigTemplate {
|
||||
templates := make([]*ConfigTemplate, 0, len(m.templates))
|
||||
for _, template := range m.templates {
|
||||
templates = append(templates, template)
|
||||
}
|
||||
return templates
|
||||
}
|
||||
|
||||
// ApplyTemplate 应用模板到配置
|
||||
func (m *ConfigTemplateManager) ApplyTemplate(templateName string, config map[string]interface{}) error {
|
||||
template, err := m.GetTemplate(templateName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 将模板配置合并到目标配置中
|
||||
for key, value := range template.Config {
|
||||
if _, exists := config[key]; !exists {
|
||||
config[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTemplateFromConfig 从配置创建模板
|
||||
func (m *ConfigTemplateManager) CreateTemplateFromConfig(name, description string, config map[string]interface{}) *ConfigTemplate {
|
||||
return &ConfigTemplate{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Config: config,
|
||||
Version: "1.0.0",
|
||||
}
|
||||
}
|
||||
|
||||
// ToJSON 将模板转换为JSON
|
||||
func (t *ConfigTemplate) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(t)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON创建模板
|
||||
func (t *ConfigTemplate) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, t)
|
||||
}
|
||||
79
plugin/config/validator.go
Normal file
79
plugin/config/validator.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// ConfigValidator 配置验证器
|
||||
type ConfigValidator struct {
|
||||
schema *ConfigSchema
|
||||
}
|
||||
|
||||
// NewConfigValidator 创建新的配置验证器
|
||||
func NewConfigValidator(schema *ConfigSchema) *ConfigValidator {
|
||||
return &ConfigValidator{
|
||||
schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证配置
|
||||
func (v *ConfigValidator) Validate(config map[string]interface{}) error {
|
||||
if v.schema == nil {
|
||||
return fmt.Errorf("no schema provided for validation")
|
||||
}
|
||||
|
||||
return v.schema.Validate(config)
|
||||
}
|
||||
|
||||
// ValidateField 验证单个字段
|
||||
func (v *ConfigValidator) ValidateField(key string, value interface{}) error {
|
||||
if v.schema == nil {
|
||||
return fmt.Errorf("no schema provided for validation")
|
||||
}
|
||||
|
||||
field, exists := v.schema.GetField(key)
|
||||
if !exists {
|
||||
return fmt.Errorf("field '%s' not found in schema", key)
|
||||
}
|
||||
|
||||
return v.schema.validateField(field, value)
|
||||
}
|
||||
|
||||
// ApplyDefaults 应用默认值
|
||||
func (v *ConfigValidator) ApplyDefaults(config map[string]interface{}) {
|
||||
if v.schema == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, field := range v.schema.Fields {
|
||||
if field.Required && field.Default != nil {
|
||||
if _, exists := config[field.Key]; !exists {
|
||||
config[field.Key] = field.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetSchema 获取配置模式
|
||||
func (v *ConfigValidator) GetSchema() *ConfigSchema {
|
||||
return v.schema
|
||||
}
|
||||
|
||||
// ValidatePattern 验证字符串模式
|
||||
func (v *ConfigValidator) ValidatePattern(pattern, value string) error {
|
||||
if pattern == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
matched, err := regexp.MatchString(pattern, value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pattern: %v", err)
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return fmt.Errorf("value '%s' does not match pattern '%s'", value, pattern)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
148
plugin/config/version.go
Normal file
148
plugin/config/version.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConfigVersion 配置版本
|
||||
type ConfigVersion struct {
|
||||
Version string `json:"version"`
|
||||
Config map[string]interface{} `json:"config"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigVersionManager 配置版本管理器
|
||||
type ConfigVersionManager struct {
|
||||
versions map[string][]*ConfigVersion // plugin_name -> versions
|
||||
maxVersions int // 最大保留版本数
|
||||
}
|
||||
|
||||
// NewConfigVersionManager 创建新的配置版本管理器
|
||||
func NewConfigVersionManager(maxVersions int) *ConfigVersionManager {
|
||||
if maxVersions <= 0 {
|
||||
maxVersions = 10 // 默认保留10个版本
|
||||
}
|
||||
|
||||
return &ConfigVersionManager{
|
||||
versions: make(map[string][]*ConfigVersion),
|
||||
maxVersions: maxVersions,
|
||||
}
|
||||
}
|
||||
|
||||
// SaveVersion 保存配置版本
|
||||
func (m *ConfigVersionManager) SaveVersion(pluginName, version, description, author string, config map[string]interface{}) error {
|
||||
if pluginName == "" {
|
||||
return fmt.Errorf("plugin name cannot be empty")
|
||||
}
|
||||
|
||||
// 创建配置副本以避免引用问题
|
||||
configCopy := make(map[string]interface{})
|
||||
for k, v := range config {
|
||||
configCopy[k] = v
|
||||
}
|
||||
|
||||
configVersion := &ConfigVersion{
|
||||
Version: version,
|
||||
Config: configCopy,
|
||||
CreatedAt: time.Now(),
|
||||
Description: description,
|
||||
Author: author,
|
||||
}
|
||||
|
||||
// 添加到版本历史
|
||||
if _, exists := m.versions[pluginName]; !exists {
|
||||
m.versions[pluginName] = make([]*ConfigVersion, 0)
|
||||
}
|
||||
|
||||
m.versions[pluginName] = append(m.versions[pluginName], configVersion)
|
||||
|
||||
// 限制版本数量
|
||||
m.limitVersions(pluginName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVersion 获取指定版本的配置
|
||||
func (m *ConfigVersionManager) GetVersion(pluginName, version string) (*ConfigVersion, error) {
|
||||
versions, exists := m.versions[pluginName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no versions found for plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
for _, v := range versions {
|
||||
if v.Version == version {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("version '%s' not found for plugin '%s'", version, pluginName)
|
||||
}
|
||||
|
||||
// GetLatestVersion 获取最新版本的配置
|
||||
func (m *ConfigVersionManager) GetLatestVersion(pluginName string) (*ConfigVersion, error) {
|
||||
versions, exists := m.versions[pluginName]
|
||||
if !exists || len(versions) == 0 {
|
||||
return nil, fmt.Errorf("no versions found for plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
// 返回最后一个(最新)版本
|
||||
return versions[len(versions)-1], nil
|
||||
}
|
||||
|
||||
// ListVersions 列出插件的所有配置版本
|
||||
func (m *ConfigVersionManager) ListVersions(pluginName string) ([]*ConfigVersion, error) {
|
||||
versions, exists := m.versions[pluginName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no versions found for plugin '%s'", pluginName)
|
||||
}
|
||||
|
||||
// 返回副本以避免外部修改
|
||||
result := make([]*ConfigVersion, len(versions))
|
||||
copy(result, versions)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RevertToVersion 回滚到指定版本
|
||||
func (m *ConfigVersionManager) RevertToVersion(pluginName, version string) (map[string]interface{}, error) {
|
||||
configVersion, err := m.GetVersion(pluginName, version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 返回配置副本
|
||||
configCopy := make(map[string]interface{})
|
||||
for k, v := range configVersion.Config {
|
||||
configCopy[k] = v
|
||||
}
|
||||
|
||||
return configCopy, nil
|
||||
}
|
||||
|
||||
// DeleteVersions 删除插件的所有版本
|
||||
func (m *ConfigVersionManager) DeleteVersions(pluginName string) {
|
||||
delete(m.versions, pluginName)
|
||||
}
|
||||
|
||||
// limitVersions 限制版本数量
|
||||
func (m *ConfigVersionManager) limitVersions(pluginName string) {
|
||||
versions := m.versions[pluginName]
|
||||
if len(versions) > m.maxVersions {
|
||||
// 保留最新的maxVersions个版本
|
||||
m.versions[pluginName] = versions[len(versions)-m.maxVersions:]
|
||||
}
|
||||
}
|
||||
|
||||
// ToJSON 将配置版本转换为JSON
|
||||
func (cv *ConfigVersion) ToJSON() ([]byte, error) {
|
||||
return json.Marshal(cv)
|
||||
}
|
||||
|
||||
// FromJSON 从JSON创建配置版本
|
||||
func (cv *ConfigVersion) FromJSON(data []byte) error {
|
||||
return json.Unmarshal(data, cv)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user