65 Commits

Author SHA1 Message Date
ctwj
b70e4eff95 update: plugin 2025-11-09 20:01:31 +08:00
ctwj
776de0bcc0 update: plugin 2025-11-09 15:36:22 +08:00
ctwj
9efe50883d update: plugin 2025-11-09 02:43:27 +08:00
Kerwin
207eb714da update: plugin 2025-11-06 16:25:31 +08:00
Kerwin
7fd33cdcd1 update: plugin 2025-11-05 18:57:33 +08:00
Kerwin
0806ef7a69 update: plugin 2025-11-04 18:30:50 +08:00
ctwj
fdb8e8a484 update: plugin 2025-11-04 01:25:34 +08:00
Kerwin
2638ccb1e4 fix: 修复nginx启动失败的问题 2025-11-03 14:11:10 +08:00
ctwj
886d91ab10 Update version history to v1.3.3 2025-11-03 14:00:21 +08:00
Kerwin
ddad95be41 update: version to 1.3.3 2025-11-03 12:29:55 +08:00
Kerwin
273800459f chore: bump version to v1.3.3 2025-11-03 11:50:08 +08:00
Kerwin
dbe24af4ac fix: docker nginx start fail 2025-11-03 11:49:33 +08:00
ctwj
a598ef508c Add entry for version 1.3.3 in ChangeLog 2025-11-03 00:00:54 +08:00
ctwj
1ca4cce6bc Merge pull request #19 from ctwj/feat_wechat
feat: 新增公众号自动回复
2025-11-02 23:56:56 +08:00
ctwj
270022188e update: 公众奥自动回复 2025-11-02 23:55:28 +08:00
Kerwin
7e80a1c2b2 update: version 1.3.2 2025-11-01 10:14:55 +08:00
Kerwin
6e7914f056 chore: bump version to v1.3.2 2025-11-01 10:09:06 +08:00
ctwj
dbde0e1675 update: wechat 2025-11-01 08:59:25 +08:00
ctwj
b840680df0 update: 完善公众号自动回复 2025-10-31 23:32:57 +08:00
ctwj
651987731b update: wechat 2025-10-31 20:14:17 +08:00
ctwj
fb26d166d6 update: bot参数 2025-10-31 16:10:32 +08:00
ctwj
8baf5c6c3d update: wechat 2025-10-31 13:36:07 +08:00
Kerwin
005aa71cc2 update: index.vue 2025-10-28 14:19:24 +08:00
Kerwin
61beed6788 update: 日志优化 2025-10-28 11:07:00 +08:00
Kerwin
53aebf2a15 add: 新增系统日志 2025-10-28 09:40:55 +08:00
ctwj
1fe9487833 update: seo优化 2025-10-28 00:33:16 +08:00
ctwj
6476ce1369 Merge pull request #17 from ctwj/feat_qrcode_1
二维码美化
2025-10-27 23:42:44 +08:00
ctwj
1ad3a07930 update: 二维码 2025-10-27 23:41:35 +08:00
Kerwin
22fd1dcf81 update: ui 2025-10-27 19:23:39 +08:00
Kerwin
f8cfe307ae Merge branch 'feat_qrcode' of https://github.com/ctwj/urldb into feat_qrcode_1 2025-10-27 19:10:46 +08:00
Kerwin
84ee0d9e53 update: qrcode 2025-10-27 19:09:13 +08:00
Kerwin
40e3350a4b opt: 优化数据库连接池,配置管理,错误处理 2025-10-27 18:21:59 +08:00
ctwj
013fe71925 Merge branch 'main' of https://github.com/ctwj/urldb 2025-10-26 10:16:59 +08:00
ctwj
6be7ae871d update: version 1.3.1 2025-10-26 10:16:52 +08:00
ctwj
89e2aca968 chore: bump version to v1.3.1 2025-10-26 10:16:00 +08:00
ctwj
f006d84b03 Update README with v1.3.1 features 2025-10-25 11:25:56 +08:00
Kerwin
7ce3839b9b chore: bump version to v1.3.1 2025-10-25 10:59:06 +08:00
ctwj
52ea019374 update: tgbot限制放开为3个 2025-10-25 09:42:19 +08:00
ctwj
4c738d1030 update: 移除Telegram Bot 中的 https://pan.l9.lc 2025-10-25 08:41:14 +08:00
ctwj
ec00f2d823 Merge branch 'main' of https://github.com/ctwj/urldb 2025-10-25 00:46:28 +08:00
ctwj
54542ff8ee update: 首页时间显示问题优化 2025-10-25 00:46:17 +08:00
ctwj
0050c6bba3 Update contact information in README.md
Removed contact section and added group chat information.
2025-10-22 16:51:43 +08:00
ctwj
4ceed8fd4b Update README with Telegram channels and links
Added links to Telegram resources and demo.
2025-10-22 16:36:34 +08:00
ctwj
2e5dd8360e update: components.d.ts 2025-10-21 00:41:16 +08:00
ctwj
40ad48f5cf update: 公告支持html 2025-10-20 23:57:27 +08:00
ctwj
921bdc43cb Update ChangeLog for version 1.3.1 2025-10-20 01:57:46 +08:00
ctwj
0df7d8bf23 add: 首页添加公告和右下角浮动按钮 2025-10-20 01:52:19 +08:00
ctwj
fdc75705aa update: 添加右下角浮动按钮 2025-10-19 13:00:19 +08:00
ctwj
a28dd4840b Merge branch 'main' of https://github.com/ctwj/urldb 2025-10-19 08:58:08 +08:00
ctwj
061b94cf61 fix: 修复首页的今日资源数不对滴问题 2025-10-19 08:56:11 +08:00
ctwj
0d28b322b7 Remove Docker build instructions from README
Removed Docker build and push instructions from README.
2025-10-19 08:39:48 +08:00
ctwj
ee06e110bd Update Telegram link in README.md 2025-10-19 08:33:54 +08:00
ctwj
7acfa300ea update: 优化tgBot 2025-10-17 00:32:25 +08:00
ctwj
b4689d2f99 Update README.md 2025-10-15 11:41:47 +08:00
Kerwin
6074d91467 update: 列表添加图片显示 2025-10-14 16:37:11 +08:00
Kerwin
e30e381adf add: default cover 2025-10-14 14:28:56 +08:00
Kerwin
516746f722 update: tgbot 优化 2025-10-10 19:17:03 +08:00
Kerwin
4da07b3ea4 update: 优化 Meilisearch tag值 2025-10-09 17:52:49 +08:00
Kerwin
da8a2ad169 Merge branch 'main' of https://github.com/ctwj/urldb 2025-10-09 17:05:03 +08:00
Kerwin
e2832b9e36 update: 删除资源时,同步删除Meilisearch中的数据 2025-10-09 17:03:03 +08:00
ctwj
bdb43531e8 update: 优化API日志显示 2025-10-07 21:57:13 +08:00
ctwj
51dbf0f03a update: 新增api访问日志 2025-10-07 02:30:01 +08:00
ctwj
10294e093f Update release.yml 2025-09-29 09:55:13 +08:00
Kerwin
6816ab0550 chore: version to 1.3.0 2025-09-29 09:41:52 +08:00
Kerwin
800b511116 add: qrcode 2025-08-25 13:05:25 +08:00
224 changed files with 34994 additions and 601 deletions

View 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": []
}
}

View File

@@ -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
View File

@@ -123,4 +123,8 @@ dist/
.dockerignore
# Air live reload
tmp/
tmp/
# plugin
plugins/
data/

View File

@@ -56,7 +56,7 @@ cp env.example .env
vim .env
# 启动开发服务器
go run main.go
go run .
```
### 前端开发

View File

@@ -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
View 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

View File

@@ -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)
---

View File

@@ -1 +1 @@
1.3.0
1.3.3

79
builtin_plugin.go Normal file
View 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
View 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
View 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
View 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层将从数据库重新加载")
}

View File

@@ -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
}

View 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
}

View File

@@ -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()
}

View File

@@ -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,
}
}

View File

@@ -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

View 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
View 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"`
}

View File

@@ -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 单个配置项

View File

@@ -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
View 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"`
}

View 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"`
}

View 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
View 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"
}

View File

@@ -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"
)

View File

@@ -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 指定表名

View 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
}

View File

@@ -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
}

View File

@@ -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
View 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
}

View 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
}

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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插件系统中的插件。

View 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 核心特性
- 支持TTLTime 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
View 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
View 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
View 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. 卸载后需要重启相关服务才能重新安装同名插件

View File

@@ -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
View 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/` 的副本,用于示例展示
- 两者内容相同,但位于不同位置以满足不同的使用需求

View 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. 总结
插件系统采用模块化设计,支持多种插件类型,具有良好的扩展性和安全性。通过标准化的接口和完整的生命周期管理,实现了灵活的插件机制。

View 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. **文档**:为插件提供使用文档

View 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
)

View 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=

View 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 文件时,此函数不会被使用
}

View 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
)

View 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=

View 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 文件时,此函数不会被使用
}

View 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 => ../../..

View 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")
}
}

View 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
View 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=

View 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

Binary file not shown.

View 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 => ../../..

View 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
}

View 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 => ../../..

View 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
View File

@@ -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
View File

@@ -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=

View 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访问日志清理成功"})
}

View File

@@ -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
View 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
View 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)),
})
}

View 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),
})
}

View File

@@ -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)
}

View File

@@ -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": "批量删除成功"})
}

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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)
// 获取账号信息,用于构建任务标题

View File

@@ -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)

View File

@@ -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
View 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
View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -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
View 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
View 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
View 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
}

View File

@@ -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
View 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
}

View 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())
}
}
}

View 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("最新版本不正确")
}
}

View 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
View 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
View 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
View 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)
}

View 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
View 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