23 Commits

Author SHA1 Message Date
ctwj
0e88374905 Merge branch 'main' of https://github.com/ctwj/urldb 2025-11-11 01:37:45 +08:00
Kerwin
081a3a7222 fix: 修复机器人停止了还能回复消息的问题 2025-11-10 10:51:33 +08:00
ctwj
6b8d2b3cf0 update: 优化推送策略 2025-11-07 23:21:04 +08:00
ctwj
9333f9da94 fix: 修复多个三方统计只生效一个的问题 2025-11-07 22:35:06 +08:00
Kerwin
806a724fb5 fix: 优化日志 2025-11-07 18:52:27 +08:00
Kerwin
487f5c9559 update: 日志优化 2025-11-07 18:50:08 +08:00
Kerwin
18b7f89c49 update: version 1.3.4 2025-11-06 20:02:29 +08:00
Kerwin
db902f3742 chore: bump version to v1.3.4 2025-11-06 19:09:48 +08:00
Kerwin
42baa891f8 fix: 修复应为推送导致的程序崩溃 2025-11-06 19:07:03 +08:00
Kerwin
02d5d00510 update: 优化平台账号管理 2025-11-05 20:52:32 +08:00
ctwj
d95c69142a Update README with WeChat auto-reply link
Added link for WeChat official account auto-reply.
2025-11-04 16:11:38 +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
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
8baf5c6c3d update: wechat 2025-10-31 13:36:07 +08:00
39 changed files with 2695 additions and 600 deletions

View File

@@ -1,3 +1,6 @@
### v1.3.3
1. 公众号自动回复
### v1.3.2
1. 二维码美化
2. TelegramBot参数调整

View File

@@ -39,11 +39,11 @@
- [服务器要求](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)
- [微信公众号自动回复](https://ecn5khs4t956.feishu.cn/wiki/APOEwOyDYicKGHk7gTzcQKpynkf?from=from_copylink)
### v1.3.2
1. 二维码美化
2. TelegramBot参数调整
3. 修复一些问题
### v1.3.3
1. 新增公众号自动回复
2. 修复一些问题
[详细改动记录](https://github.com/ctwj/urldb/blob/main/ChangeLog.md)

View File

@@ -1 +1 @@
1.3.2
1.3.4

View File

@@ -209,7 +209,15 @@ func createIndexes(db *gorm.DB) {
db.Exec("CREATE INDEX IF NOT EXISTS idx_resource_tags_resource_id ON resource_tags(resource_id)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_resource_tags_tag_id ON resource_tags(tag_id)")
utils.Info("数据库索引创建完成已移除全文搜索索引准备使用Meilisearch")
// API访问日志表索引 - 高性能查询优化
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_created_at ON api_access_logs(created_at DESC)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_endpoint_status ON api_access_logs(endpoint, response_status)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_ip_created ON api_access_logs(ip, created_at DESC)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_method_endpoint ON api_access_logs(method, endpoint)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_response_time ON api_access_logs(processing_time)")
db.Exec("CREATE INDEX IF NOT EXISTS idx_api_access_logs_error_logs ON api_access_logs(response_status, created_at DESC) WHERE response_status >= 400")
utils.Info("数据库索引创建完成已移除全文搜索索引准备使用Meilisearch新增API访问日志性能索引")
}
// insertDefaultDataIfEmpty 只在数据库为空时插入默认数据

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
}

View File

@@ -72,19 +72,20 @@ type PanResponse struct {
// CksResponse Cookie响应
type CksResponse struct {
ID uint `json:"id"`
PanID uint `json:"pan_id"`
Idx int `json:"idx"`
Ck string `json:"ck"`
IsValid bool `json:"is_valid"`
Space int64 `json:"space"`
LeftSpace int64 `json:"left_space"`
UsedSpace int64 `json:"used_space"`
Username string `json:"username"`
VipStatus bool `json:"vip_status"`
ServiceType string `json:"service_type"`
Remark string `json:"remark"`
Pan *PanResponse `json:"pan,omitempty"`
ID uint `json:"id"`
PanID uint `json:"pan_id"`
Idx int `json:"idx"`
Ck string `json:"ck"`
IsValid bool `json:"is_valid"`
Space int64 `json:"space"`
LeftSpace int64 `json:"left_space"`
UsedSpace int64 `json:"used_space"`
Username string `json:"username"`
VipStatus bool `json:"vip_status"`
ServiceType string `json:"service_type"`
Remark string `json:"remark"`
TransferredCount int64 `json:"transferred_count"` // 已转存资源数
Pan *PanResponse `json:"pan,omitempty"`
}
// ReadyResourceResponse 待处理资源响应

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

@@ -57,6 +57,16 @@ const (
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"
@@ -135,6 +145,16 @@ const (
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"
@@ -200,6 +220,16 @@ const (
ConfigDefaultTelegramProxyUsername = ""
ConfigDefaultTelegramProxyPassword = ""
// 微信公众号配置默认值
ConfigDefaultWechatBotEnabled = "false"
ConfigDefaultWechatAppId = ""
ConfigDefaultWechatAppSecret = ""
ConfigDefaultWechatToken = ""
ConfigDefaultWechatEncodingAesKey = ""
ConfigDefaultWechatWelcomeMessage = "欢迎关注老九网盘资源库!发送关键词即可搜索资源。"
ConfigDefaultWechatAutoReplyEnabled = "true"
ConfigDefaultWechatSearchLimit = "5"
// 界面配置默认值
ConfigDefaultEnableAnnouncements = "false"
ConfigDefaultAnnouncements = ""

View File

@@ -44,6 +44,8 @@ type ResourceRepository interface {
MarkAllAsUnsyncedToMeilisearch() error
FindAllWithPagination(page, limit int) ([]entity.Resource, int64, error)
GetRandomResourceWithFilters(categoryFilter, tagFilter string, isPushSavedInfo bool) (*entity.Resource, error)
DeleteRelatedResources(ckID uint) (int64, error)
CountResourcesByCkID(ckID uint) (int64, error)
}
// ResourceRepositoryImpl Resource的Repository实现
@@ -277,6 +279,20 @@ func (r *ResourceRepositoryImpl) SearchWithFilters(params map[string]interface{}
db = db.Where("pan_id = ?", panEntity.ID)
}
}
case "exclude_ids": // 添加exclude_ids参数支持
if excludeIDs, ok := value.([]uint); ok && len(excludeIDs) > 0 {
// 限制排除ID的数量避免SQL语句过长
maxExcludeIDs := 5000 // 限制排除ID数量避免SQL语句过长
if len(excludeIDs) > maxExcludeIDs {
// 只取最近的maxExcludeIDs个ID进行排除
startIndex := len(excludeIDs) - maxExcludeIDs
truncatedExcludeIDs := excludeIDs[startIndex:]
db = db.Where("id NOT IN ?", truncatedExcludeIDs)
utils.Debug("SearchWithFilters: 排除ID数量过多截取最近%d个ID", len(truncatedExcludeIDs))
} else {
db = db.Where("id NOT IN ?", excludeIDs)
}
}
}
}
@@ -650,3 +666,29 @@ func (r *ResourceRepositoryImpl) GetRandomResourceWithFilters(categoryFilter, ta
return &resource, nil
}
// DeleteRelatedResources 删除关联资源,清空 fid、ck_id 和 save_url 三个字段
func (r *ResourceRepositoryImpl) DeleteRelatedResources(ckID uint) (int64, error) {
result := r.db.Model(&entity.Resource{}).
Where("ck_id = ?", ckID).
Updates(map[string]interface{}{
"fid": nil, // 清空 fid 字段
"ck_id": 0, // 清空 ck_id 字段
"save_url": "", // 清空 save_url 字段
})
if result.Error != nil {
return 0, result.Error
}
return result.RowsAffected, nil
}
// CountResourcesByCkID 统计指定账号ID的资源数量
func (r *ResourceRepositoryImpl) CountResourcesByCkID(ckID uint) (int64, error) {
var count int64
err := r.db.Model(&entity.Resource{}).
Where("ck_id = ?", ckID).
Count(&count).Error
return count, err
}

View File

@@ -20,7 +20,7 @@ services:
- app-network
backend:
image: ctwj/urldb-backend:1.3.2
image: ctwj/urldb-backend:1.3.4
environment:
DB_HOST: postgres
DB_PORT: 5432
@@ -38,7 +38,7 @@ services:
- app-network
frontend:
image: ctwj/urldb-frontend:1.3.2
image: ctwj/urldb-frontend:1.3.4
environment:
NODE_ENV: production
NUXT_PUBLIC_API_SERVER: http://backend:8080/api

10
go.mod
View File

@@ -21,13 +21,23 @@ require (
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/fatih/structs v1.1.0 // 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_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/silenceper/wechat/v2 v2.1.10 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/cast v1.4.1 // 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
)

108
go.sum
View File

@@ -1,14 +1,22 @@
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
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=
@@ -16,6 +24,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 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
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/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=
@@ -38,8 +52,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=
@@ -49,11 +66,26 @@ 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/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=
@@ -100,6 +132,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
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/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/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/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=
@@ -121,10 +165,18 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
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 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
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=
@@ -132,6 +184,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
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/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=
@@ -140,32 +198,64 @@ 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/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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.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.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.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
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=
@@ -175,8 +265,20 @@ 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=
@@ -187,6 +289,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
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/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
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/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

@@ -22,7 +22,49 @@ func GetCks(c *gin.Context) {
return
}
responses := converter.ToCksResponseList(cks)
// 使用新的逻辑创建 CksResponse
var responses []dto.CksResponse
for _, ck := range cks {
// 获取平台信息
var pan *dto.PanResponse
if ck.PanID != 0 {
panEntity, err := repoManager.PanRepository.FindByID(ck.PanID)
if err == nil && panEntity != nil {
pan = &dto.PanResponse{
ID: panEntity.ID,
Name: panEntity.Name,
Key: panEntity.Key,
Icon: panEntity.Icon,
Remark: panEntity.Remark,
}
}
}
// 统计转存资源数
count, err := repoManager.ResourceRepository.CountResourcesByCkID(ck.ID)
if err != nil {
count = 0 // 统计失败时设为0
}
response := dto.CksResponse{
ID: ck.ID,
PanID: ck.PanID,
Idx: ck.Idx,
Ck: ck.Ck,
IsValid: ck.IsValid,
Space: ck.Space,
LeftSpace: ck.LeftSpace,
UsedSpace: ck.UsedSpace,
Username: ck.Username,
VipStatus: ck.VipStatus,
ServiceType: ck.ServiceType,
Remark: ck.Remark,
TransferredCount: count,
Pan: pan,
}
responses = append(responses, response)
}
SuccessResponse(c, responses)
}
@@ -380,3 +422,25 @@ func RefreshCapacity(c *gin.Context) {
"cks": converter.ToCksResponse(cks),
})
}
// DeleteRelatedResources 删除关联资源
func DeleteRelatedResources(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
ErrorResponse(c, "无效的ID", http.StatusBadRequest)
return
}
// 调用资源库删除关联资源
affectedRows, err := repoManager.ResourceRepository.DeleteRelatedResources(uint(id))
if err != nil {
ErrorResponse(c, "删除关联资源失败: "+err.Error(), http.StatusInternalServerError)
return
}
SuccessResponse(c, gin.H{
"message": "关联资源删除成功",
"affected_rows": affectedRows,
})
}

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
}

View File

@@ -1,6 +1,7 @@
package handlers
import (
"encoding/json"
"strconv"
"strings"
"time"
@@ -91,30 +92,9 @@ func (h *PublicAPIHandler) logAPIAccess(c *gin.Context, startTime time.Time, pro
}
}
// 异步记录日志避免影响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)
}
}()
// 记录API访问日志 - 使用简单日志记录
h.recordAPIAccessToDB(ip, userAgent, endpoint, method, requestParams,
c.Writer.Status(), responseData, processCount, errorMessage, processingTime)
}
// AddBatchResources godoc
@@ -466,3 +446,49 @@ func (h *PublicAPIHandler) GetHotDramas(c *gin.Context) {
h.logAPIAccess(c, startTime, len(hotDramaResponses), responseData, "")
SuccessResponse(c, responseData)
}
// recordAPIAccessToDB 记录API访问日志到数据库
func (h *PublicAPIHandler) recordAPIAccessToDB(ip, userAgent, endpoint, method string,
requestParams interface{}, responseStatus int, responseData interface{},
processCount int, errorMessage string, processingTime int64) {
// 只记录重要的API访问有错误或处理时间较长的
if errorMessage == "" && processingTime < 1000 && responseStatus < 400 {
return // 跳过正常的快速请求
}
// 转换参数为JSON字符串
var requestParamsStr, responseDataStr string
if requestParams != nil {
if jsonBytes, err := json.Marshal(requestParams); err == nil {
requestParamsStr = string(jsonBytes)
}
}
if responseData != nil {
if jsonBytes, err := json.Marshal(responseData); err == nil {
responseDataStr = string(jsonBytes)
}
}
// 创建日志记录
logEntry := &entity.APIAccessLog{
IP: ip,
UserAgent: userAgent,
Endpoint: endpoint,
Method: method,
RequestParams: requestParamsStr,
ResponseStatus: responseStatus,
ResponseData: responseDataStr,
ProcessCount: processCount,
ErrorMessage: errorMessage,
ProcessingTime: processingTime,
}
// 异步保存到数据库避免影响API性能
go func() {
if err := repoManager.APIAccessLogRepository.Create(logEntry); err != nil {
// 记录失败只输出到系统日志不影响API
utils.Error("保存API访问日志失败: %v", err)
}
}()
}

View File

@@ -145,27 +145,26 @@ func UpdateSystemConfig(c *gin.Context) {
utils.Info("当前配置数量: %d", len(currentConfigs))
}
// 验证参数 - 只验证提交的字段
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 {
utils.Warn("配置验证失败 - SiteTitle长度无效: %d", len(*req.SiteTitle))
ErrorResponse(c, "网站标题长度必须在1-100字符之间", http.StatusBadRequest)
return
}
}
if req.AutoProcessInterval != nil {
utils.Info("验证AutoProcessInterval: %d", *req.AutoProcessInterval)
if *req.AutoProcessInterval < 1 || *req.AutoProcessInterval > 1440 {
utils.Warn("配置验证失败 - AutoProcessInterval超出范围: %d", *req.AutoProcessInterval)
ErrorResponse(c, "自动处理间隔必须在1-1440分钟之间", http.StatusBadRequest)
return
}
}
if req.PageSize != nil {
utils.Info("验证PageSize: %d", *req.PageSize)
if *req.PageSize < 10 || *req.PageSize > 500 {
utils.Warn("配置验证失败 - PageSize超出范围: %d", *req.PageSize)
ErrorResponse(c, "每页显示数量必须在10-500之间", http.StatusBadRequest)
return
}
@@ -173,16 +172,16 @@ func UpdateSystemConfig(c *gin.Context) {
// 验证自动转存配置
if req.AutoTransferLimitDays != nil {
utils.Info("验证AutoTransferLimitDays: %d", *req.AutoTransferLimitDays)
if *req.AutoTransferLimitDays < 0 || *req.AutoTransferLimitDays > 365 {
utils.Warn("配置验证失败 - AutoTransferLimitDays超出范围: %d", *req.AutoTransferLimitDays)
ErrorResponse(c, "自动转存限制天数必须在0-365之间", http.StatusBadRequest)
return
}
}
if req.AutoTransferMinSpace != nil {
utils.Info("验证AutoTransferMinSpace: %d", *req.AutoTransferMinSpace)
if *req.AutoTransferMinSpace < 100 || *req.AutoTransferMinSpace > 1024 {
utils.Warn("配置验证失败 - AutoTransferMinSpace超出范围: %d", *req.AutoTransferMinSpace)
ErrorResponse(c, "最小存储空间必须在100-1024GB之间", http.StatusBadRequest)
return
}
@@ -190,19 +189,17 @@ func UpdateSystemConfig(c *gin.Context) {
// 验证公告相关字段
if req.Announcements != nil {
utils.Info("验证Announcements: '%s'", *req.Announcements)
// 可以在这里添加更详细的验证逻辑
// 简化验证,仅在需要时添加逻辑
}
// 转换为实体
configs := converter.RequestToSystemConfig(&req)
if configs == nil {
utils.Error("配置数据转换失败")
ErrorResponse(c, "数据转换失败", http.StatusInternalServerError)
return
}
utils.Info("准备更新配置,配置项数量: %d", len(configs))
// 保存配置
err = repoManager.SystemConfigRepository.UpsertConfigs(configs)
if err != nil {
@@ -211,7 +208,7 @@ func UpdateSystemConfig(c *gin.Context) {
return
}
utils.Info("配置保存成功")
utils.Info("系统配置更新成功 - 更新项数: %d", len(configs))
// 安全刷新系统配置缓存
if err := repoManager.SystemConfigRepository.SafeRefreshConfigCache(); err != nil {

View File

@@ -80,16 +80,40 @@ func (h *TelegramHandler) UpdateBotConfig(c *gin.Context) {
return
}
// 配置更新完成后,尝试启动机器人(如果未运行且配置有效)
if startErr := h.telegramBotService.Start(); startErr != nil {
utils.Warn("[TELEGRAM:HANDLER] 配置更新后尝试启动机器人失败: %v", startErr)
// 启动失败不影响配置保存,只记录警告
// 根据配置状态决定启动或停止机器人
botEnabled := false
for _, config := range configs {
if config.Key == "telegram_bot_enabled" {
botEnabled = config.Value == "true"
break
}
}
if botEnabled {
// 机器人已启用,尝试启动机器人
if startErr := h.telegramBotService.Start(); startErr != nil {
utils.Warn("[TELEGRAM:HANDLER] 配置更新后尝试启动机器人失败: %v", startErr)
// 启动失败不影响配置保存,只记录警告
}
} else {
// 机器人已禁用,停止机器人服务
if stopErr := h.telegramBotService.Stop(); stopErr != nil {
utils.Warn("[TELEGRAM:HANDLER] 配置更新后停止机器人失败: %v", stopErr)
// 停止失败不影响配置保存,只记录警告
}
}
// 返回成功
var message string
if botEnabled {
message = "配置更新成功,机器人已尝试启动"
} else {
message = "配置更新成功,机器人已停止"
}
SuccessResponse(c, map[string]interface{}{
"success": true,
"message": "配置更新成功,机器人已尝试启动",
"message": message,
})
}

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

57
main.go
View File

@@ -4,7 +4,9 @@ import (
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/ctwj/urldb/config"
@@ -38,7 +40,7 @@ func main() {
}
// 初始化日志系统
if err := utils.InitLogger(nil); err != nil {
if err := utils.InitLogger(); err != nil {
log.Fatal("初始化日志系统失败:", err)
}
@@ -84,6 +86,8 @@ func main() {
utils.Fatal("数据库连接失败: %v", err)
}
// 日志系统已简化,无需额外初始化
// 创建Repository管理器
repoManager := repo.NewRepositoryManager(db.DB)
@@ -152,9 +156,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)
@@ -259,6 +269,7 @@ func main() {
api.DELETE("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteCks)
api.GET("/cks/:id", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.GetCksByID)
api.POST("/cks/:id/refresh-capacity", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.RefreshCapacity)
api.POST("/cks/:id/delete-related-resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), handlers.DeleteRelatedResources)
// 标签管理
api.GET("/tags", handlers.GetTags)
@@ -363,6 +374,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(
@@ -377,6 +390,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,
@@ -398,6 +423,17 @@ 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)
}
// 设置监控系统
@@ -431,6 +467,21 @@ func main() {
port = "8080"
}
utils.Info("服务器启动在端口 %s", port)
r.Run(":" + port)
// 设置优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 在goroutine中启动服务器
go func() {
utils.Info("服务器启动在端口 %s", port)
if err := r.Run(":" + port); err != nil && err.Error() != "http: Server closed" {
utils.Fatal("服务器启动失败: %v", err)
}
}()
// 等待信号
<-quit
utils.Info("收到关闭信号,开始优雅关闭...")
utils.Info("服务器已优雅关闭")
}

View File

@@ -34,7 +34,7 @@ func AuthMiddleware() gin.HandlerFunc {
c.Request.Method, c.Request.URL.Path, clientIP, userAgent)
if authHeader == "" {
utils.Warn("AuthMiddleware - 未提供认证令牌 - IP: %s, Path: %s", clientIP, c.Request.URL.Path)
// utils.Warn("AuthMiddleware - 未提供认证令牌 - IP: %s, Path: %s", clientIP, c.Request.URL.Path)
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
c.Abort()
return
@@ -59,8 +59,8 @@ func AuthMiddleware() gin.HandlerFunc {
return
}
utils.Info("AuthMiddleware - 认证成功 - 用户: %s(ID:%d), 角色: %s, IP: %s",
claims.Username, claims.UserID, claims.Role, clientIP)
// utils.Info("AuthMiddleware - 认证成功 - 用户: %s(ID:%d), 角色: %s, IP: %s",
// claims.Username, claims.UserID, claims.Role, clientIP)
// 将用户信息存储到上下文中
c.Set("user_id", claims.UserID)

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"io"
"net/http"
"strings"
"time"
"github.com/ctwj/urldb/utils"
@@ -55,41 +56,64 @@ func LoggingMiddleware(next http.Handler) http.Handler {
})
}
// logRequest 记录请求日志
// logRequest 记录请求日志 - 优化后仅记录异常和关键请求
func logRequest(r *http.Request, rw *responseWriter, duration time.Duration, requestBody []byte) {
// 获取客户端IP
clientIP := getClientIP(r)
// 获取用户代理
userAgent := r.UserAgent()
if userAgent == "" {
userAgent = "Unknown"
// 判断是否需要记录日志的条件
shouldLog := rw.statusCode >= 400 || // 错误状态码
duration > 5*time.Second || // 耗时过长
shouldLogPath(r.URL.Path) || // 关键路径
isAdminPath(r.URL.Path) // 管理员路径
if !shouldLog {
return // 正常请求不记录日志,减少日志噪音
}
// 记录请求信息
utils.Info("HTTP请求 - %s %s - IP: %s - User-Agent: %s - 状态码: %d - 耗时: %v",
r.Method, r.URL.Path, clientIP, userAgent, rw.statusCode, duration)
// 如果是错误状态码,记录详细信息
// 简化的日志格式移除User-Agent以减少噪音
if rw.statusCode >= 400 {
utils.Error("HTTP错误 - %s %s - 状态码: %d - 响应体: %s",
r.Method, r.URL.Path, rw.statusCode, rw.body.String())
// 错误请求记录详细信息
utils.Error("HTTP异常 - %s %s - IP: %s - 状态码: %d - 耗时: %v",
r.Method, r.URL.Path, clientIP, rw.statusCode, duration)
// 仅在错误状态下记录简要的请求信息
if len(requestBody) > 0 && len(requestBody) <= 500 {
utils.Error("请求详情: %s", string(requestBody))
}
} else if duration > 5*time.Second {
// 慢请求警告
utils.Warn("HTTP慢请求 - %s %s - IP: %s - 耗时: %v",
r.Method, r.URL.Path, clientIP, duration)
} else {
// 关键路径的正常请求
utils.Info("HTTP关键请求 - %s %s - IP: %s - 状态码: %d - 耗时: %v",
r.Method, r.URL.Path, clientIP, rw.statusCode, duration)
}
}
// shouldLogPath 判断路径是否需要记录日志
func shouldLogPath(path string) bool {
// 定义需要记录日志的关键路径
keyPaths := []string{
"/api/public/resources",
"/api/admin/config",
"/api/admin/users",
"/telegram/webhook",
}
// 记录请求参数仅对POST/PUT请求
if (r.Method == "POST" || r.Method == "PUT") && len(requestBody) > 0 {
// 限制日志长度,避免日志文件过大
if len(requestBody) > 1000 {
utils.Debug("请求体(截断): %s...", string(requestBody[:1000]))
} else {
utils.Debug("请求体: %s", string(requestBody))
for _, keyPath := range keyPaths {
if strings.HasPrefix(path, keyPath) {
return true
}
}
return false
}
// 记录查询参数
if len(r.URL.RawQuery) > 0 {
utils.Debug("查询参数: %s", r.URL.RawQuery)
}
// isAdminPath 判断是否为管理员路径
func isAdminPath(path string) bool {
return strings.HasPrefix(path, "/api/admin/") ||
strings.HasPrefix(path, "/admin/")
}
// getClientIP 获取客户端真实IP地址

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;

102
services/base.go Normal file
View File

@@ -0,0 +1,102 @@
package services
import (
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/db/repo"
"github.com/ctwj/urldb/utils"
)
var repoManager *repo.RepositoryManager
var meilisearchManager *MeilisearchManager
// SetRepositoryManager 设置Repository管理器
func SetRepositoryManager(manager *repo.RepositoryManager) {
repoManager = manager
}
// SetMeilisearchManager 设置Meilisearch管理器
func SetMeilisearchManager(manager *MeilisearchManager) {
meilisearchManager = manager
}
// UnifiedSearchResources 执行统一搜索优先使用Meilisearch否则使用数据库搜索并处理违禁词
func UnifiedSearchResources(keyword string, limit int, systemConfigRepo repo.SystemConfigRepository, resourceRepo repo.ResourceRepository) ([]entity.Resource, error) {
var resources []entity.Resource
var total int64
var err error
// 如果启用了Meilisearch优先使用Meilisearch搜索
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
// 构建MeiliSearch过滤器
filters := make(map[string]interface{})
// 使用Meilisearch搜索
service := meilisearchManager.GetService()
if service != nil {
docs, docTotal, err := service.Search(keyword, filters, 1, limit)
if err == nil {
// 将Meilisearch文档转换为Resource实体
for _, doc := range docs {
resource := entity.Resource{
ID: doc.ID,
Title: doc.Title,
Description: doc.Description,
URL: doc.URL,
SaveURL: doc.SaveURL,
FileSize: doc.FileSize,
Key: doc.Key,
PanID: doc.PanID,
CreatedAt: doc.CreatedAt,
UpdatedAt: doc.UpdatedAt,
}
resources = append(resources, resource)
}
total = docTotal
// 获取违禁词配置并处理违禁词
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
return systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
})
if err != nil {
utils.Error("获取违禁词配置失败: %v", err)
cleanWords = []string{} // 如果获取失败,使用空列表
}
// 处理违禁词替换
if len(cleanWords) > 0 {
resources = utils.ProcessResourcesForbiddenWords(resources, cleanWords)
}
return resources, nil
} else {
utils.Error("MeiliSearch搜索失败回退到数据库搜索: %v", err)
}
}
}
// 如果MeiliSearch未启用、搜索失败或没有搜索关键词使用数据库搜索
resources, total, err = resourceRepo.Search(keyword, nil, 1, limit)
if err != nil {
return nil, err
}
if total == 0 {
return []entity.Resource{}, nil
}
// 获取违禁词配置并处理违禁词
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
return systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
})
if err != nil {
utils.Error("获取违禁词配置失败: %v", err)
cleanWords = []string{} // 如果获取失败,使用空列表
}
// 处理违禁词替换
if len(cleanWords) > 0 {
resources = utils.ProcessResourcesForbiddenWords(resources, cleanWords)
}
return resources, nil
}

231
services/search_session.go Normal file
View File

@@ -0,0 +1,231 @@
package services
import (
"sync"
"time"
"github.com/ctwj/urldb/db/entity"
)
// SearchSession 搜索会话
type SearchSession struct {
UserID string // 用户ID
Keyword string // 搜索关键字
Resources []entity.Resource // 搜索结果
PageSize int // 每页数量
CurrentPage int // 当前页码
TotalPages int // 总页数
LastAccess time.Time // 最后访问时间
}
// SearchSessionManager 搜索会话管理器
type SearchSessionManager struct {
sessions map[string]*SearchSession // 用户ID -> 搜索会话
mutex sync.RWMutex
}
// NewSearchSessionManager 创建搜索会话管理器
func NewSearchSessionManager() *SearchSessionManager {
manager := &SearchSessionManager{
sessions: make(map[string]*SearchSession),
}
// 启动清理过期会话的goroutine
go manager.cleanupExpiredSessions()
return manager
}
// CreateSession 创建或更新搜索会话
func (m *SearchSessionManager) CreateSession(userID, keyword string, resources []entity.Resource, pageSize int) *SearchSession {
m.mutex.Lock()
defer m.mutex.Unlock()
session := &SearchSession{
UserID: userID,
Keyword: keyword,
Resources: resources,
PageSize: pageSize,
CurrentPage: 1,
TotalPages: (len(resources) + pageSize - 1) / pageSize,
LastAccess: time.Now(),
}
m.sessions[userID] = session
return session
}
// GetSession 获取搜索会话
func (m *SearchSessionManager) GetSession(userID string) *SearchSession {
m.mutex.RLock()
defer m.mutex.RUnlock()
session, exists := m.sessions[userID]
if !exists {
return nil
}
// 更新最后访问时间
m.mutex.RUnlock()
m.mutex.Lock()
session.LastAccess = time.Now()
m.mutex.Unlock()
m.mutex.RLock()
return session
}
// SetCurrentPage 设置当前页
func (m *SearchSessionManager) SetCurrentPage(userID string, page int) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
session, exists := m.sessions[userID]
if !exists {
return false
}
if page < 1 || page > session.TotalPages {
return false
}
session.CurrentPage = page
session.LastAccess = time.Now()
return true
}
// GetPageResources 获取指定页的资源
func (m *SearchSessionManager) GetPageResources(userID string, page int) []entity.Resource {
m.mutex.RLock()
session, exists := m.sessions[userID]
m.mutex.RUnlock()
if !exists {
return nil
}
if page < 1 || page > session.TotalPages {
return nil
}
start := (page - 1) * session.PageSize
end := start + session.PageSize
if end > len(session.Resources) {
end = len(session.Resources)
}
// 更新当前页和最后访问时间
m.mutex.Lock()
session.CurrentPage = page
session.LastAccess = time.Now()
m.mutex.Unlock()
return session.Resources[start:end]
}
// GetCurrentPageResources 获取当前页的资源
func (m *SearchSessionManager) GetCurrentPageResources(userID string) []entity.Resource {
m.mutex.RLock()
session, exists := m.sessions[userID]
m.mutex.RUnlock()
if !exists {
return nil
}
return m.GetPageResources(userID, session.CurrentPage)
}
// HasNextPage 是否有下一页
func (m *SearchSessionManager) HasNextPage(userID string) bool {
m.mutex.RLock()
defer m.mutex.RUnlock()
session, exists := m.sessions[userID]
if !exists {
return false
}
return session.CurrentPage < session.TotalPages
}
// HasPrevPage 是否有上一页
func (m *SearchSessionManager) HasPrevPage(userID string) bool {
m.mutex.RLock()
defer m.mutex.RUnlock()
session, exists := m.sessions[userID]
if !exists {
return false
}
return session.CurrentPage > 1
}
// NextPage 下一页
func (m *SearchSessionManager) NextPage(userID string) []entity.Resource {
m.mutex.Lock()
session, exists := m.sessions[userID]
m.mutex.Unlock()
if !exists {
return nil
}
if session.CurrentPage >= session.TotalPages {
return nil
}
return m.GetPageResources(userID, session.CurrentPage+1)
}
// PrevPage 上一页
func (m *SearchSessionManager) PrevPage(userID string) []entity.Resource {
m.mutex.Lock()
session, exists := m.sessions[userID]
m.mutex.Unlock()
if !exists {
return nil
}
if session.CurrentPage <= 1 {
return nil
}
return m.GetPageResources(userID, session.CurrentPage-1)
}
// GetPageInfo 获取分页信息
func (m *SearchSessionManager) GetPageInfo(userID string) (currentPage, totalPages int, hasPrev, hasNext bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
session, exists := m.sessions[userID]
if !exists {
return 0, 0, false, false
}
return session.CurrentPage, session.TotalPages, session.CurrentPage > 1, session.CurrentPage < session.TotalPages
}
// cleanupExpiredSessions 清理过期会话超过1小时未访问
func (m *SearchSessionManager) cleanupExpiredSessions() {
ticker := time.NewTicker(10 * time.Minute)
defer ticker.Stop()
for range ticker.C {
m.mutex.Lock()
now := time.Now()
for userID, session := range m.sessions {
// 如果超过1小时未访问清理该会话
if now.Sub(session.LastAccess) > time.Hour {
delete(m.sessions, userID)
}
}
m.mutex.Unlock()
}
}
// GlobalSearchSessionManager 全局搜索会话管理器
var GlobalSearchSessionManager = NewSearchSessionManager()

View File

@@ -52,6 +52,7 @@ type TelegramBotServiceImpl struct {
config *TelegramBotConfig
pushHistory map[int64][]uint // 每个频道的推送历史记录最多100条
mu sync.RWMutex // 用于保护pushHistory的读写锁
stopChan chan struct{} // 用于停止消息循环的channel
}
type TelegramBotConfig struct {
@@ -84,6 +85,7 @@ func NewTelegramBotService(
cronScheduler: cron.New(),
config: &TelegramBotConfig{},
pushHistory: make(map[int64][]uint),
stopChan: make(chan struct{}),
}
}
@@ -111,57 +113,55 @@ func (s *TelegramBotServiceImpl) loadConfig() error {
s.config.ProxyUsername = ""
s.config.ProxyPassword = ""
// 统计配置项数量,用于汇总日志
configCount := 0
for _, config := range configs {
switch config.Key {
case entity.ConfigKeyTelegramBotEnabled:
s.config.Enabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (Enabled: %v)", config.Key, config.Value, s.config.Enabled)
case entity.ConfigKeyTelegramBotApiKey:
s.config.ApiKey = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
case entity.ConfigKeyTelegramAutoReplyEnabled:
s.config.AutoReplyEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoReplyEnabled: %v)", config.Key, config.Value, s.config.AutoReplyEnabled)
case entity.ConfigKeyTelegramAutoReplyTemplate:
if config.Value != "" {
s.config.AutoReplyTemplate = config.Value
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, config.Value)
case entity.ConfigKeyTelegramAutoDeleteEnabled:
s.config.AutoDeleteEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoDeleteEnabled: %v)", config.Key, config.Value, s.config.AutoDeleteEnabled)
case entity.ConfigKeyTelegramAutoDeleteInterval:
if config.Value != "" {
fmt.Sscanf(config.Value, "%d", &s.config.AutoDeleteInterval)
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (AutoDeleteInterval: %d)", config.Key, config.Value, s.config.AutoDeleteInterval)
case entity.ConfigKeyTelegramProxyEnabled:
s.config.ProxyEnabled = config.Value == "true"
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyEnabled: %v)", config.Key, config.Value, s.config.ProxyEnabled)
case entity.ConfigKeyTelegramProxyType:
s.config.ProxyType = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyType: %s)", config.Key, config.Value, s.config.ProxyType)
case entity.ConfigKeyTelegramProxyHost:
s.config.ProxyHost = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
case entity.ConfigKeyTelegramProxyPort:
if config.Value != "" {
fmt.Sscanf(config.Value, "%d", &s.config.ProxyPort)
}
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s (ProxyPort: %d)", config.Key, config.Value, s.config.ProxyPort)
case entity.ConfigKeyTelegramProxyUsername:
s.config.ProxyUsername = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
case entity.ConfigKeyTelegramProxyPassword:
s.config.ProxyPassword = config.Value
utils.Info("[TELEGRAM:CONFIG] 加载配置 %s = %s", config.Key, "[HIDDEN]")
default:
utils.Debug("未知配置: %s = %s", config.Key, config.Value)
utils.Debug("未知Telegram配置: %s", config.Key)
}
configCount++
}
utils.Info("[TELEGRAM:SERVICE] Telegram Bot 配置加载完成: Enabled=%v, AutoReplyEnabled=%v, ApiKey长度=%d",
s.config.Enabled, s.config.AutoReplyEnabled, len(s.config.ApiKey))
// 汇总输出配置加载结果,避免逐项日志
proxyStatus := "禁用"
if s.config.ProxyEnabled {
proxyStatus = "启用"
}
utils.TelegramInfo("配置加载完成 - Bot启用: %v, 自动回复: %v, 代理: %s, 配置项数: %d",
s.config.Enabled, s.config.AutoReplyEnabled, proxyStatus, configCount)
return nil
}
@@ -185,6 +185,11 @@ func (s *TelegramBotServiceImpl) Start() error {
if !s.config.Enabled || s.config.ApiKey == "" {
utils.Info("[TELEGRAM:SERVICE] Telegram Bot 未启用或 API Key 未配置")
// 如果机器人当前正在运行,需要停止它
if s.isRunning {
utils.Info("[TELEGRAM:SERVICE] 机器人已被禁用,停止正在运行的服务")
s.Stop()
}
return nil
}
@@ -259,6 +264,9 @@ func (s *TelegramBotServiceImpl) Start() error {
s.bot = bot
s.isRunning = true
// 重置停止信号channel
s.stopChan = make(chan struct{})
utils.Info("[TELEGRAM:SERVICE] Telegram Bot (@%s) 已启动", s.GetBotUsername())
// 启动推送调度器
@@ -283,6 +291,15 @@ func (s *TelegramBotServiceImpl) Stop() error {
s.isRunning = false
// 安全地发送停止信号给消息循环
select {
case <-s.stopChan:
// channel 已经关闭
default:
// channel 未关闭,安全关闭
close(s.stopChan)
}
if s.cronScheduler != nil {
s.cronScheduler.Stop()
}
@@ -514,20 +531,34 @@ func (s *TelegramBotServiceImpl) messageLoop() {
utils.Info("[TELEGRAM:MESSAGE] 消息监听循环已启动,等待消息...")
for update := range updates {
if update.Message != nil {
utils.Info("[TELEGRAM:MESSAGE] 接收到新消息更新")
s.handleMessage(update.Message)
} else {
utils.Debug("[TELEGRAM:MESSAGE] 接收到其他类型更新: %v", update)
for {
select {
case <-s.stopChan:
utils.Info("[TELEGRAM:MESSAGE] 收到停止信号,退出消息监听循环")
return
case update, ok := <-updates:
if !ok {
utils.Info("[TELEGRAM:MESSAGE] updates channel 已关闭,退出消息监听循环")
return
}
if update.Message != nil {
utils.Info("[TELEGRAM:MESSAGE] 接收到新消息更新")
s.handleMessage(update.Message)
} else {
utils.Debug("[TELEGRAM:MESSAGE] 接收到其他类型更新: %v", update)
}
}
}
utils.Info("[TELEGRAM:MESSAGE] 消息监听循环已结束")
}
// handleMessage 处理接收到的消息
func (s *TelegramBotServiceImpl) handleMessage(message *tgbotapi.Message) {
// 检查机器人是否正在运行且已启用
if !s.isRunning || !s.config.Enabled {
utils.Info("[TELEGRAM:MESSAGE] 机器人已停止或禁用,跳过消息处理: ChatID=%d", message.Chat.ID)
return
}
chatID := message.Chat.ID
text := strings.TrimSpace(message.Text)
@@ -965,8 +996,17 @@ func (s *TelegramBotServiceImpl) pushToChannel(channel entity.TelegramChannel) {
// 5. 记录推送的资源ID到历史记录避免重复推送
for _, resource := range resources {
resourceEntity := resource.(entity.Resource)
s.addPushedResourceID(channel.ChatID, resourceEntity.ID)
var resourceID uint
switch r := resource.(type) {
case *entity.Resource:
resourceID = r.ID
case entity.Resource:
resourceID = r.ID
default:
utils.Error("[TELEGRAM:PUSH] 无效的资源类型: %T", resource)
continue
}
s.addPushedResourceID(channel.ChatID, resourceID)
}
utils.Info("[TELEGRAM:PUSH:SUCCESS] 成功推送内容到频道: %s (%d 条资源)", channel.ChatName, len(resources))
@@ -1009,16 +1049,16 @@ func (s *TelegramBotServiceImpl) findResourcesForChannel(channel entity.Telegram
func (s *TelegramBotServiceImpl) findLatestResources(channel entity.TelegramChannel, excludeResourceIDs []uint) []interface{} {
params := s.buildFilterParams(channel)
// 在数据库查询中排除已推送的资源
if len(excludeResourceIDs) > 0 {
params["exclude_ids"] = excludeResourceIDs
}
// 使用现有的搜索功能,按更新时间倒序获取最新资源
resources, _, err := s.resourceRepo.SearchWithFilters(params)
if err != nil {
utils.Error("[TELEGRAM:PUSH] 获取最新资源失败: %v", err)
return []interface{}{}
}
// 排除最近推送过的资源
if len(excludeResourceIDs) > 0 {
resources = s.excludePushedResources(resources, excludeResourceIDs)
return s.findRandomResources(channel, excludeResourceIDs) // 回退到随机策略
}
// 应用时间限制
@@ -1027,13 +1067,13 @@ func (s *TelegramBotServiceImpl) findLatestResources(channel entity.TelegramChan
}
if len(resources) == 0 {
utils.Info("[TELEGRAM:PUSH] 没有找到符合条件的最新资源")
return []interface{}{}
utils.Info("[TELEGRAM:PUSH] 没有找到符合条件的最新资源,尝试获取随机资源")
return s.findRandomResources(channel, excludeResourceIDs) // 回退到随机策略
}
// 返回最新资源(第一条)
utils.Info("[TELEGRAM:PUSH] 成功获取最新资源: %s", resources[0].Title)
return []interface{}{resources[0]}
return []interface{}{&resources[0]}
}
// findTransferredResources 查找已转存资源
@@ -1043,6 +1083,11 @@ func (s *TelegramBotServiceImpl) findTransferredResources(channel entity.Telegra
// 添加转存链接条件
params["has_save_url"] = true
// 在数据库查询中排除已推送的资源
if len(excludeResourceIDs) > 0 {
params["exclude_ids"] = excludeResourceIDs
}
// 优先获取有转存链接的资源
resources, _, err := s.resourceRepo.SearchWithFilters(params)
if err != nil {
@@ -1050,11 +1095,6 @@ func (s *TelegramBotServiceImpl) findTransferredResources(channel entity.Telegra
return []interface{}{}
}
// 排除最近推送过的资源
if len(excludeResourceIDs) > 0 {
resources = s.excludePushedResources(resources, excludeResourceIDs)
}
// 应用时间限制
if channel.TimeLimit != "none" && len(resources) > 0 {
resources = s.applyTimeFilter(resources, channel.TimeLimit)
@@ -1068,7 +1108,7 @@ func (s *TelegramBotServiceImpl) findTransferredResources(channel entity.Telegra
// 返回第一个有转存链接的资源
utils.Info("[TELEGRAM:PUSH] 成功获取已转存资源: %s", resources[0].Title)
return []interface{}{resources[0]}
return []interface{}{&resources[0]}
}
// findRandomResources 查找随机资源(原有逻辑)
@@ -1078,23 +1118,19 @@ func (s *TelegramBotServiceImpl) findRandomResources(channel entity.TelegramChan
// 如果是已转存优先策略但没有找到转存资源,这里会回退到随机策略
// 此时不需要额外的转存链接条件,让随机函数处理
// 先尝试获取候选资源列表,然后从中排除已推送的资源
var candidateResources []entity.Resource
var err error
// 在数据库查询中排除已推送的资源
if len(excludeResourceIDs) > 0 {
params["exclude_ids"] = excludeResourceIDs
}
// 使用搜索功能获取候选资源,然后过滤
params["limit"] = 100 // 获取更多候选资源
candidateResources, _, err = s.resourceRepo.SearchWithFilters(params)
candidateResources, _, err := s.resourceRepo.SearchWithFilters(params)
if err != nil {
utils.Error("[TELEGRAM:PUSH] 获取候选资源失败: %v", err)
return []interface{}{}
}
// 排除最近推送过的资源
if len(excludeResourceIDs) > 0 {
candidateResources = s.excludePushedResources(candidateResources, excludeResourceIDs)
}
// 应用时间限制
if channel.TimeLimit != "none" && len(candidateResources) > 0 {
candidateResources = s.applyTimeFilter(candidateResources, channel.TimeLimit)
@@ -1108,7 +1144,7 @@ func (s *TelegramBotServiceImpl) findRandomResources(channel entity.TelegramChan
utils.Info("[TELEGRAM:PUSH] 成功获取随机资源: %s (从 %d 个候选资源中选择)",
selectedResource.Title, len(candidateResources))
return []interface{}{selectedResource}
return []interface{}{&selectedResource}
}
// 如果候选资源不足,回退到数据库随机函数
@@ -1184,7 +1220,18 @@ func (s *TelegramBotServiceImpl) buildFilterParams(channel entity.TelegramChanne
// buildPushMessage 构建推送消息
func (s *TelegramBotServiceImpl) buildPushMessage(channel entity.TelegramChannel, resources []interface{}) (string, string) {
resource := resources[0].(entity.Resource)
var resource *entity.Resource
// 处理两种可能的类型:*entity.Resource 或 entity.Resource
switch r := resources[0].(type) {
case *entity.Resource:
resource = r
case entity.Resource:
resource = &r
default:
utils.Error("[TELEGRAM:PUSH] 无效的资源类型: %T", resources[0])
return "", ""
}
message := fmt.Sprintf("🆕 <b>%s</b>\n", s.cleanMessageTextForHTML(resource.Title))
@@ -1243,6 +1290,12 @@ func (s *TelegramBotServiceImpl) GetBotUsername() string {
// SendMessage 发送消息(默认使用 HTML 格式)
func (s *TelegramBotServiceImpl) SendMessage(chatID int64, text string, img string) error {
// 检查机器人是否正在运行且已启用
if !s.isRunning || !s.config.Enabled {
utils.Info("[TELEGRAM:MESSAGE] 机器人已停止或禁用,跳过发送消息: ChatID=%d", chatID)
return fmt.Errorf("机器人已停止或禁用")
}
if img == "" {
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = "HTML"
@@ -1752,11 +1805,12 @@ func (s *TelegramBotServiceImpl) addPushedResourceID(chatID int64, resourceID ui
history = []uint{}
}
// 检查是否已经超过100条记录
if len(history) >= 100 {
// 清空历史记录,重新开始
history = []uint{}
utils.Info("[TELEGRAM:PUSH] 频道 %d 推送历史记录已满(100条),清空重置", chatID)
// 检查是否已经超过5000条记录
if len(history) >= 5000 {
// 移除旧的2500条记录保留最新的2500条记录
startIndex := len(history) - 2500
history = history[startIndex:]
utils.Info("[TELEGRAM:PUSH] 频道 %d 推送历史记录已满(5000条)移除旧的2500条记录保留最新的2500条", chatID)
}
// 添加新的资源ID到历史记录
@@ -1839,10 +1893,11 @@ func (s *TelegramBotServiceImpl) loadPushHistory() error {
resourceIDs = append(resourceIDs, uint(resourceID))
}
// 只保留最多100条记录
if len(resourceIDs) > 100 {
// 保留最新的100条记录
resourceIDs = resourceIDs[len(resourceIDs)-100:]
// 只保留最多5000条记录
if len(resourceIDs) > 5000 {
// 保留最新的5000条记录
startIndex := len(resourceIDs) - 5000
resourceIDs = resourceIDs[startIndex:]
}
s.pushHistory[chatID] = resourceIDs

View File

@@ -0,0 +1,58 @@
package services
import (
"github.com/ctwj/urldb/db/repo"
"github.com/silenceper/wechat/v2/officialaccount"
"github.com/silenceper/wechat/v2/officialaccount/message"
)
// WechatBotService 微信公众号机器人服务接口
type WechatBotService interface {
Start() error
Stop() error
IsRunning() bool
ReloadConfig() error
HandleMessage(msg *message.MixMessage) (interface{}, error)
SendWelcomeMessage(openID string) error
GetRuntimeStatus() map[string]interface{}
GetConfig() *WechatBotConfig
}
// WechatBotConfig 微信公众号机器人配置
type WechatBotConfig struct {
Enabled bool
AppID string
AppSecret string
Token string
EncodingAesKey string
WelcomeMessage string
AutoReplyEnabled bool
SearchLimit int
}
// WechatBotServiceImpl 微信公众号机器人服务实现
type WechatBotServiceImpl struct {
isRunning bool
systemConfigRepo repo.SystemConfigRepository
resourceRepo repo.ResourceRepository
readyRepo repo.ReadyResourceRepository
config *WechatBotConfig
wechatClient *officialaccount.OfficialAccount
searchSessionManager *SearchSessionManager
}
// NewWechatBotService 创建微信公众号机器人服务
func NewWechatBotService(
systemConfigRepo repo.SystemConfigRepository,
resourceRepo repo.ResourceRepository,
readyResourceRepo repo.ReadyResourceRepository,
) WechatBotService {
return &WechatBotServiceImpl{
isRunning: false,
systemConfigRepo: systemConfigRepo,
resourceRepo: resourceRepo,
readyRepo: readyResourceRepo,
config: &WechatBotConfig{},
searchSessionManager: GlobalSearchSessionManager,
}
}

View File

@@ -0,0 +1,524 @@
package services
import (
"fmt"
"strconv"
"strings"
"github.com/ctwj/urldb/db/entity"
"github.com/ctwj/urldb/utils"
"github.com/silenceper/wechat/v2/cache"
"github.com/silenceper/wechat/v2/officialaccount"
"github.com/silenceper/wechat/v2/officialaccount/config"
"github.com/silenceper/wechat/v2/officialaccount/message"
)
// loadConfig 加载微信配置
func (s *WechatBotServiceImpl) loadConfig() error {
configs, err := s.systemConfigRepo.GetOrCreateDefault()
if err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
utils.Info("[WECHAT] 从数据库加载到 %d 个配置项", len(configs))
// 初始化默认值
s.config.Enabled = false
s.config.AppID = ""
s.config.AppSecret = ""
s.config.Token = ""
s.config.EncodingAesKey = ""
s.config.WelcomeMessage = "欢迎关注老九网盘资源库!发送关键词即可搜索资源。"
s.config.AutoReplyEnabled = true
s.config.SearchLimit = 5
for _, config := range configs {
switch config.Key {
case entity.ConfigKeyWechatBotEnabled:
s.config.Enabled = config.Value == "true"
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (Enabled: %v)", config.Key, config.Value, s.config.Enabled)
case entity.ConfigKeyWechatAppId:
s.config.AppID = config.Value
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
case entity.ConfigKeyWechatAppSecret:
s.config.AppSecret = config.Value
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
case entity.ConfigKeyWechatToken:
s.config.Token = config.Value
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
case entity.ConfigKeyWechatEncodingAesKey:
s.config.EncodingAesKey = config.Value
utils.Info("[WECHAT:CONFIG] 加载配置 %s = [HIDDEN]", config.Key)
case entity.ConfigKeyWechatWelcomeMessage:
if config.Value != "" {
s.config.WelcomeMessage = config.Value
}
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s", config.Key, config.Value)
case entity.ConfigKeyWechatAutoReplyEnabled:
s.config.AutoReplyEnabled = config.Value == "true"
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (AutoReplyEnabled: %v)", config.Key, config.Value, s.config.AutoReplyEnabled)
case entity.ConfigKeyWechatSearchLimit:
if config.Value != "" {
limit, err := strconv.Atoi(config.Value)
if err == nil && limit > 0 {
s.config.SearchLimit = limit
}
}
utils.Info("[WECHAT:CONFIG] 加载配置 %s = %s (SearchLimit: %d)", config.Key, config.Value, s.config.SearchLimit)
}
}
utils.Info("[WECHAT:SERVICE] 微信公众号机器人配置加载完成: Enabled=%v, AutoReplyEnabled=%v",
s.config.Enabled, s.config.AutoReplyEnabled)
return nil
}
// Start 启动微信公众号机器人服务
func (s *WechatBotServiceImpl) Start() error {
if s.isRunning {
utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已经在运行中")
return nil
}
// 加载配置
if err := s.loadConfig(); err != nil {
return fmt.Errorf("加载配置失败: %v", err)
}
if !s.config.Enabled || s.config.AppID == "" || s.config.AppSecret == "" {
utils.Info("[WECHAT:SERVICE] 微信公众号机器人未启用或配置不完整")
return nil
}
// 创建微信客户端
cfg := &config.Config{
AppID: s.config.AppID,
AppSecret: s.config.AppSecret,
Token: s.config.Token,
EncodingAESKey: s.config.EncodingAesKey,
Cache: cache.NewMemory(),
}
s.wechatClient = officialaccount.NewOfficialAccount(cfg)
s.isRunning = true
utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已启动")
return nil
}
// Stop 停止微信公众号机器人服务
func (s *WechatBotServiceImpl) Stop() error {
if !s.isRunning {
return nil
}
s.isRunning = false
utils.Info("[WECHAT:SERVICE] 微信公众号机器人服务已停止")
return nil
}
// IsRunning 检查微信公众号机器人服务是否正在运行
func (s *WechatBotServiceImpl) IsRunning() bool {
return s.isRunning
}
// ReloadConfig 重新加载微信公众号机器人配置
func (s *WechatBotServiceImpl) ReloadConfig() error {
utils.Info("[WECHAT:SERVICE] 开始重新加载配置...")
// 重新加载配置
if err := s.loadConfig(); err != nil {
utils.Error("[WECHAT:SERVICE] 重新加载配置失败: %v", err)
return fmt.Errorf("重新加载配置失败: %v", err)
}
utils.Info("[WECHAT:SERVICE] 配置重新加载完成: Enabled=%v, AutoReplyEnabled=%v",
s.config.Enabled, s.config.AutoReplyEnabled)
return nil
}
// GetRuntimeStatus 获取微信公众号机器人运行时状态
func (s *WechatBotServiceImpl) GetRuntimeStatus() map[string]interface{} {
status := map[string]interface{}{
"is_running": s.IsRunning(),
"config_loaded": s.config != nil,
"app_id": s.config.AppID,
}
return status
}
// GetConfig 获取当前配置
func (s *WechatBotServiceImpl) GetConfig() *WechatBotConfig {
return s.config
}
// HandleMessage 处理微信消息
func (s *WechatBotServiceImpl) HandleMessage(msg *message.MixMessage) (interface{}, error) {
utils.Info("[WECHAT:MESSAGE] 收到消息: FromUserName=%s, MsgType=%s, Event=%s, Content=%s",
msg.FromUserName, msg.MsgType, msg.Event, msg.Content)
switch msg.MsgType {
case message.MsgTypeText:
return s.handleTextMessage(msg)
case message.MsgTypeEvent:
return s.handleEventMessage(msg)
default:
return nil, nil // 不处理其他类型消息
}
}
// handleTextMessage 处理文本消息
func (s *WechatBotServiceImpl) handleTextMessage(msg *message.MixMessage) (interface{}, error) {
utils.Debug("[WECHAT:MESSAGE] 处理文本消息 - AutoReplyEnabled: %v", s.config.AutoReplyEnabled)
if !s.config.AutoReplyEnabled {
utils.Info("[WECHAT:MESSAGE] 自动回复未启用")
return nil, nil
}
keyword := strings.TrimSpace(msg.Content)
utils.Info("[WECHAT:MESSAGE] 搜索关键词: '%s'", keyword)
// 检查是否是分页命令
if keyword == "上一页" || keyword == "prev" {
return s.handlePrevPage(string(msg.FromUserName))
}
if keyword == "下一页" || keyword == "next" {
return s.handleNextPage(string(msg.FromUserName))
}
// 检查是否是获取命令(例如:获取 1, 获取2等
if strings.HasPrefix(keyword, "获取") || strings.HasPrefix(keyword, "get") {
return s.handleGetResource(string(msg.FromUserName), keyword)
}
if keyword == "" {
utils.Info("[WECHAT:MESSAGE] 关键词为空,返回提示消息")
return message.NewText("请输入搜索关键词"), nil
}
// 检查搜索关键词是否包含违禁词
cleanWords, err := utils.GetForbiddenWordsFromConfig(func() (string, error) {
return s.systemConfigRepo.GetConfigValue(entity.ConfigKeyForbiddenWords)
})
if err != nil {
utils.Error("获取违禁词配置失败: %v", err)
cleanWords = []string{} // 如果获取失败,使用空列表
}
// 检查关键词是否包含违禁词
if len(cleanWords) > 0 {
containsForbidden, matchedWords := utils.CheckContainsForbiddenWords(keyword, cleanWords)
if containsForbidden {
utils.Info("[WECHAT:MESSAGE] 搜索关键词包含违禁词: %v", matchedWords)
return message.NewText("您的搜索关键词包含违禁内容,不予处理"), nil
}
}
// 搜索资源
utils.Debug("[WECHAT:MESSAGE] 开始搜索资源,限制数量: %d", s.config.SearchLimit)
resources, err := s.SearchResources(keyword)
if err != nil {
utils.Error("[WECHAT:SEARCH] 搜索失败: %v", err)
return message.NewText("搜索服务暂时不可用,请稍后重试"), nil
}
utils.Info("[WECHAT:MESSAGE] 搜索完成,找到 %d 个资源", len(resources))
if len(resources) == 0 {
utils.Info("[WECHAT:MESSAGE] 未找到相关资源,返回提示消息")
return message.NewText(fmt.Sprintf("未找到关键词\"%s\"相关的资源,请尝试其他关键词", keyword)), nil
}
// 创建搜索会话并保存第一页结果
s.searchSessionManager.CreateSession(string(msg.FromUserName), keyword, resources, 4)
pageResources := s.searchSessionManager.GetCurrentPageResources(string(msg.FromUserName))
// 格式化第一页搜索结果
resultText := s.formatSearchResultsWithPagination(keyword, pageResources, string(msg.FromUserName))
utils.Info("[WECHAT:MESSAGE] 格式化搜索结果,返回文本长度: %d", len(resultText))
return message.NewText(resultText), nil
}
// handlePrevPage 处理上一页命令
func (s *WechatBotServiceImpl) handlePrevPage(userID string) (interface{}, error) {
session := s.searchSessionManager.GetSession(userID)
if session == nil {
return message.NewText("没有找到搜索记录,请先进行搜索"), nil
}
if !s.searchSessionManager.HasPrevPage(userID) {
return message.NewText("已经是第一页了"), nil
}
prevResources := s.searchSessionManager.PrevPage(userID)
if prevResources == nil {
return message.NewText("获取上一页失败"), nil
}
currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID)
resultText := s.formatPageResources(session.Keyword, prevResources, currentPage, totalPages, userID)
return message.NewText(resultText), nil
}
// handleNextPage 处理下一页命令
func (s *WechatBotServiceImpl) handleNextPage(userID string) (interface{}, error) {
session := s.searchSessionManager.GetSession(userID)
if session == nil {
return message.NewText("没有找到搜索记录,请先进行搜索"), nil
}
if !s.searchSessionManager.HasNextPage(userID) {
return message.NewText("已经是最后一页了"), nil
}
nextResources := s.searchSessionManager.NextPage(userID)
if nextResources == nil {
return message.NewText("获取下一页失败"), nil
}
currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID)
resultText := s.formatPageResources(session.Keyword, nextResources, currentPage, totalPages, userID)
return message.NewText(resultText), nil
}
// handleGetResource 处理获取资源命令
func (s *WechatBotServiceImpl) handleGetResource(userID, command string) (interface{}, error) {
session := s.searchSessionManager.GetSession(userID)
if session == nil {
return message.NewText("没有找到搜索记录,请先进行搜索"), nil
}
// 检查是否只输入了"获取"或"get",没有指定编号
if command == "获取" || command == "get" {
return message.NewText("📌 请输入要获取的资源编号\n\n💡 提示:回复\"获取 1\"或\"get 1\"获取第一个资源的详细信息"), nil
}
// 解析命令,例如:"获取 1" 或 "get 2"
// 支持"获取4"这种没有空格的格式
var index int
_, err := fmt.Sscanf(command, "获取%d", &index)
if err != nil {
_, err = fmt.Sscanf(command, "获取 %d", &index)
if err != nil {
_, err = fmt.Sscanf(command, "get%d", &index)
if err != nil {
_, err = fmt.Sscanf(command, "get %d", &index)
if err != nil {
return message.NewText("❌ 命令格式错误\n\n📌 正确格式:\n • 获取 1\n • get 1\n • 获取1\n • get1"), nil
}
}
}
}
if index < 1 || index > len(session.Resources) {
return message.NewText(fmt.Sprintf("❌ 资源编号超出范围\n\n📌 请输入 1-%d 之间的数字\n💡 提示:回复\"获取 %d\"获取第%d个资源", len(session.Resources), index, index)), nil
}
// 获取指定资源
resource := session.Resources[index-1]
// 格式化资源详细信息(美化输出)
var result strings.Builder
// result.WriteString(fmt.Sprintf("📌 资源详情\n\n"))
// 标题
result.WriteString(fmt.Sprintf("📌 标题: %s\n", resource.Title))
// 描述
if resource.Description != "" {
result.WriteString(fmt.Sprintf("\n📝 描述:\n %s\n", resource.Description))
}
// 文件大小
if resource.FileSize != "" {
result.WriteString(fmt.Sprintf("\n📊 大小: %s\n", resource.FileSize))
}
// 作者
if resource.Author != "" {
result.WriteString(fmt.Sprintf("\n👤 作者: %s\n", resource.Author))
}
// 分类
if resource.Category.Name != "" {
result.WriteString(fmt.Sprintf("\n📂 分类: %s\n", resource.Category.Name))
}
// 标签
if len(resource.Tags) > 0 {
result.WriteString("\n🏷 标签: ")
var tags []string
for _, tag := range resource.Tags {
tags = append(tags, tag.Name)
}
result.WriteString(fmt.Sprintf("%s\n", strings.Join(tags, " ")))
}
// 链接(美化)
if resource.SaveURL != "" {
result.WriteString(fmt.Sprintf("\n📥 转存链接:\n %s", resource.SaveURL))
} else if resource.URL != "" {
result.WriteString(fmt.Sprintf("\n🔗 资源链接:\n %s", resource.URL))
}
// 添加操作提示
result.WriteString(fmt.Sprintf("\n\n💡 提示:回复\"获取 %d\"可再次查看此资源", index))
return message.NewText(result.String()), nil
}
// formatSearchResultsWithPagination 格式化带分页的搜索结果
func (s *WechatBotServiceImpl) formatSearchResultsWithPagination(keyword string, resources []entity.Resource, userID string) string {
currentPage, totalPages, _, _ := s.searchSessionManager.GetPageInfo(userID)
return s.formatPageResources(keyword, resources, currentPage, totalPages, userID)
}
// formatPageResources 格式化页面资源
// 根据用户需求,搜索结果中不显示资源链接,只显示标题和描述
func (s *WechatBotServiceImpl) formatPageResources(keyword string, resources []entity.Resource, currentPage, totalPages int, userID string) string {
var result strings.Builder
result.WriteString(fmt.Sprintf("🔍 搜索\"%s\"的结果(第%d/%d页\n\n", keyword, currentPage, totalPages))
for i, resource := range resources {
// 构建当前资源的文本表示
var resourceText strings.Builder
// 计算全局索引当前页的第i个资源在整个结果中的位置
globalIndex := (currentPage-1)*4 + i + 1
resourceText.WriteString(fmt.Sprintf("%d. 📌 %s\n", globalIndex, resource.Title))
if resource.Description != "" {
// 限制描述长度以避免消息过长(正确处理中文字符)
desc := resource.Description
// 将字符串转换为 rune 切片以正确处理中文字符
runes := []rune(desc)
if len(runes) > 50 {
desc = string(runes[:50]) + "..."
}
resourceText.WriteString(fmt.Sprintf(" 📝 %s\n", desc))
}
// 添加标签显示(格式:🏷️标签,空格,再接别的标签)
if len(resource.Tags) > 0 {
var tags []string
for _, tag := range resource.Tags {
tags = append(tags, "🏷️"+tag.Name)
}
// 限制标签数量以避免消息过长
if len(tags) > 5 {
tags = tags[:5]
}
resourceText.WriteString(fmt.Sprintf(" %s\n", strings.Join(tags, " ")))
}
resourceText.WriteString(fmt.Sprintf(" 👉 回复\"获取 %d\"查看详细信息\n", globalIndex))
resourceText.WriteString("\n")
// 预计算添加当前资源后的消息长度
tempMessage := result.String() + resourceText.String()
// 添加分页提示和预留空间
if currentPage > 1 || currentPage < totalPages {
tempMessage += "💡 提示:回复\""
if currentPage > 1 && currentPage < totalPages {
tempMessage += "上一页\"或\"下一页"
} else if currentPage > 1 {
tempMessage += "上一页"
} else {
tempMessage += "下一页"
}
tempMessage += "\"翻页\n"
}
// 检查添加当前资源后是否会超过微信限制
tempRunes := []rune(tempMessage)
if len(tempRunes) > 550 {
result.WriteString("💡 内容较多,请翻页查看更多\n")
break
}
// 如果不会超过限制,则添加当前资源到结果中
result.WriteString(resourceText.String())
}
// 添加分页提示
var pageTips []string
if currentPage > 1 {
pageTips = append(pageTips, "上一页")
}
if currentPage < totalPages {
pageTips = append(pageTips, "下一页")
}
if len(pageTips) > 0 {
result.WriteString(fmt.Sprintf("💡 提示:回复\"%s\"翻页\n", strings.Join(pageTips, "\"或\"")))
}
// 确保消息不超过微信限制(正确处理中文字符)
message := result.String()
// 将字符串转换为 rune 切片以正确处理中文字符
runes := []rune(message)
if len(runes) > 600 {
// 如果还是超过限制截断消息微信建议不超过600个字符
message = string(runes[:597]) + "..."
}
return message
}
// handleEventMessage 处理事件消息
func (s *WechatBotServiceImpl) handleEventMessage(msg *message.MixMessage) (interface{}, error) {
if msg.Event == message.EventSubscribe {
// 新用户关注
return message.NewText(s.config.WelcomeMessage), nil
}
return nil, nil
}
// SearchResources 搜索资源
func (s *WechatBotServiceImpl) SearchResources(keyword string) ([]entity.Resource, error) {
// 使用统一搜索函数包含Meilisearch优先搜索和违禁词处理
return UnifiedSearchResources(keyword, s.config.SearchLimit, s.systemConfigRepo, s.resourceRepo)
}
// formatSearchResults 格式化搜索结果
func (s *WechatBotServiceImpl) formatSearchResults(keyword string, resources []entity.Resource) string {
var result strings.Builder
result.WriteString(fmt.Sprintf("🔍 搜索\"%s\"的结果(共%d条\n\n", keyword, len(resources)))
for i, resource := range resources {
result.WriteString(fmt.Sprintf("%d. %s\n", i+1, resource.Title))
if resource.Cover != "" {
result.WriteString(fmt.Sprintf(" ![封面](%s)\n", resource.Cover))
}
if resource.Description != "" {
desc := resource.Description
if len(desc) > 50 {
desc = desc[:50] + "..."
}
result.WriteString(fmt.Sprintf(" %s\n", desc))
}
if resource.SaveURL != "" {
result.WriteString(fmt.Sprintf(" 转存链接:%s\n", resource.SaveURL))
} else if resource.URL != "" {
result.WriteString(fmt.Sprintf(" 资源链接:%s\n", resource.URL))
}
result.WriteString("\n")
}
result.WriteString("💡 提示:回复资源编号可获取详细信息")
return result.String()
}
// SendWelcomeMessage 发送欢迎消息(预留接口,实际通过事件处理)
func (s *WechatBotServiceImpl) SendWelcomeMessage(openID string) error {
// 实际上欢迎消息是通过关注事件自动发送的
// 这里提供一个手动发送的接口
if !s.isRunning || s.wechatClient == nil {
return fmt.Errorf("微信客户端未初始化")
}
// 注意Customer API 需要额外的权限,这里仅作示例
// 实际应用中可能需要使用模板消息或其他方式
return nil
}

View File

@@ -189,12 +189,22 @@ func (tm *TaskManager) StopTask(taskID uint) error {
// processTask 处理任务
func (tm *TaskManager) processTask(ctx context.Context, task *entity.Task, processor TaskProcessor) {
startTime := utils.GetCurrentTime()
// 记录任务开始
utils.Info("任务开始 - ID: %d, 类型: %s", task.ID, task.Type)
defer func() {
tm.mu.Lock()
delete(tm.running, task.ID)
tm.mu.Unlock()
elapsedTime := time.Since(startTime)
utils.Info("processTask: 任务 %d 处理完成,耗时: %v清理资源", task.ID, elapsedTime)
// 使用业务事件记录任务完成,只有异常情况才输出详细日志
if elapsedTime > 30*time.Second {
utils.Warn("任务处理耗时较长 - ID: %d, 类型: %s, 耗时: %v", task.ID, task.Type, elapsedTime)
}
utils.Info("任务完成 - ID: %d, 类型: %s, 耗时: %v", task.ID, task.Type, elapsedTime)
}()
utils.InfoWithFields(map[string]interface{}{

View File

@@ -130,7 +130,7 @@ func (p *ForbiddenWordsProcessor) ProcessForbiddenWords(text string, forbiddenWo
// ParseForbiddenWordsConfig 解析违禁词配置字符串
// 参数:
// - config: 违禁词配置字符串,多个词用逗号分隔
// - config: 违禁词配置字符串,多个词用逗号或换行符分隔
//
// 返回:
// - []string: 处理后的违禁词列表
@@ -139,16 +139,21 @@ func (p *ForbiddenWordsProcessor) ParseForbiddenWordsConfig(config string) []str
return nil
}
words := strings.Split(config, ",")
var cleanWords []string
for _, word := range words {
word = strings.TrimSpace(word)
if word != "" {
cleanWords = append(cleanWords, word)
var words []string
// 首先尝试用换行符分割
lines := strings.Split(config, "\n")
for _, line := range lines {
// 对每一行再用逗号分割(兼容两种格式)
parts := strings.Split(line, ",")
for _, part := range parts {
word := strings.TrimSpace(part)
if word != "" {
words = append(words, word)
}
}
}
return cleanWords
return words
}
// 全局实例,方便直接调用

View File

@@ -1,7 +1,6 @@
package utils
import (
"encoding/json"
"fmt"
"io"
"log"
@@ -10,7 +9,6 @@ import (
"runtime"
"strings"
"sync"
"time"
)
// LogLevel 日志级别
@@ -24,7 +22,7 @@ const (
FATAL
)
// String 返回日志级别的字符串表示
// String 返回级别的字符串表示
func (l LogLevel) String() string {
switch l {
case DEBUG:
@@ -42,280 +40,76 @@ func (l LogLevel) String() string {
}
}
// StructuredLogEntry 结构化日志条目
type StructuredLogEntry struct {
Timestamp time.Time `json:"timestamp"`
Level string `json:"level"`
Message string `json:"message"`
Caller string `json:"caller"`
Module string `json:"module"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
// Logger 统一日志器
// Logger 简化的日志器
type Logger struct {
debugLogger *log.Logger
infoLogger *log.Logger
warnLogger *log.Logger
errorLogger *log.Logger
fatalLogger *log.Logger
level LogLevel
logger *log.Logger
file *os.File
mu sync.Mutex
config *LogConfig
}
// LogConfig 日志配置
type LogConfig struct {
LogDir string // 日志目录
LogLevel LogLevel // 日志级别
MaxFileSize int64 // 单个日志文件最大大小MB
MaxBackups int // 最大备份文件数
MaxAge int // 日志文件最大保留天数
EnableConsole bool // 是否启用控制台输出
EnableFile bool // 是否启用文件输出
EnableRotation bool // 是否启用日志轮转
StructuredLog bool // 是否启用结构化日志格式
}
// DefaultConfig 默认配置
func DefaultConfig() *LogConfig {
// 从环境变量获取日志级别默认为INFO
logLevel := getLogLevelFromEnv()
return &LogConfig{
LogDir: "logs",
LogLevel: logLevel,
MaxFileSize: 100, // 100MB
MaxBackups: 5,
MaxAge: 30, // 30天
EnableConsole: true,
EnableFile: true,
EnableRotation: true,
StructuredLog: os.Getenv("STRUCTURED_LOG") == "true", // 从环境变量控制结构化日志
}
}
// getLogLevelFromEnv 从环境变量获取日志级别
func getLogLevelFromEnv() LogLevel {
envLogLevel := os.Getenv("LOG_LEVEL")
envDebug := os.Getenv("DEBUG")
// 如果设置了DEBUG环境变量为true则使用DEBUG级别
if envDebug == "true" || envDebug == "1" {
return DEBUG
}
// 根据LOG_LEVEL环境变量设置日志级别
switch strings.ToUpper(envLogLevel) {
case "DEBUG":
return DEBUG
case "INFO":
return INFO
case "WARN", "WARNING":
return WARN
case "ERROR":
return ERROR
case "FATAL":
return FATAL
default:
// 根据运行环境设置默认级别开发环境DEBUG生产环境INFO
if isDevelopment() {
return DEBUG
}
return INFO
}
}
// isDevelopment 判断是否为开发环境
func isDevelopment() bool {
env := os.Getenv("GO_ENV")
return env == "development" || env == "dev" || env == "local" || env == "test"
}
// getEnvironment 获取当前环境类型
func (l *Logger) getEnvironment() string {
if isDevelopment() {
return "development"
}
return "production"
mu sync.RWMutex
}
var (
globalLogger *Logger
onceLogger sync.Once
loggerOnce sync.Once
)
// InitLogger 初始化全局日志器
func InitLogger(config *LogConfig) error {
// InitLogger 初始化日志器
func InitLogger() error {
var err error
onceLogger.Do(func() {
if config == nil {
config = DefaultConfig()
loggerOnce.Do(func() {
globalLogger = &Logger{
level: INFO,
logger: log.New(os.Stdout, "", log.LstdFlags),
}
globalLogger, err = NewLogger(config)
// 创建日志目录
logDir := "logs"
if err = os.MkdirAll(logDir, 0755); err != nil {
return
}
// 创建日志文件
logFile := filepath.Join(logDir, "app.log")
globalLogger.file, err = os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return
}
// 同时输出到控制台和文件
globalLogger.logger = log.New(io.MultiWriter(os.Stdout, globalLogger.file), "", log.LstdFlags)
})
return err
}
// GetLogger 获取全局日志器
func GetLogger() *Logger {
if globalLogger == nil {
InitLogger(nil)
InitLogger()
}
return globalLogger
}
// NewLogger 创建新的日志器
func NewLogger(config *LogConfig) (*Logger, error) {
if config == nil {
config = DefaultConfig()
}
logger := &Logger{
config: config,
}
// 创建日志目录
if config.EnableFile {
if err := os.MkdirAll(config.LogDir, 0755); err != nil {
return nil, fmt.Errorf("创建日志目录失败: %v", err)
}
}
// 初始化日志文件
if err := logger.initLogFile(); err != nil {
return nil, err
}
// 初始化日志器
logger.initLoggers()
// 启动日志轮转检查
if config.EnableRotation {
go logger.startRotationCheck()
}
// 打印日志配置信息
logger.Info("日志系统初始化完成 - 级别: %s, 环境: %s",
config.LogLevel.String(),
logger.getEnvironment())
return logger, nil
}
// initLogFile 初始化日志文件
func (l *Logger) initLogFile() error {
if !l.config.EnableFile {
return nil
}
l.mu.Lock()
defer l.mu.Unlock()
// 关闭现有文件
if l.file != nil {
l.file.Close()
}
// 创建新的日志文件
logFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s.log", GetCurrentTime().Format("2006-01-02")))
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return fmt.Errorf("创建日志文件失败: %v", err)
}
l.file = file
return nil
}
// initLoggers 初始化各个级别的日志器
func (l *Logger) initLoggers() {
var writers []io.Writer
// 添加控制台输出
if l.config.EnableConsole {
writers = append(writers, os.Stdout)
}
// 添加文件输出
if l.config.EnableFile && l.file != nil {
writers = append(writers, l.file)
}
multiWriter := io.MultiWriter(writers...)
// 创建各个级别的日志器
l.debugLogger = log.New(multiWriter, "[DEBUG] ", log.LstdFlags)
l.infoLogger = log.New(multiWriter, "[INFO] ", log.LstdFlags)
l.warnLogger = log.New(multiWriter, "[WARN] ", log.LstdFlags)
l.errorLogger = log.New(multiWriter, "[ERROR] ", log.LstdFlags)
l.fatalLogger = log.New(multiWriter, "[FATAL] ", log.LstdFlags)
}
// log 内部日志方法
func (l *Logger) log(level LogLevel, format string, args ...interface{}) {
if level < l.config.LogLevel {
if level < l.level {
return
}
// 获取调用者信息
_, file, line, ok := runtime.Caller(2)
if !ok {
file = "unknown"
line = 0
caller := "unknown"
if ok {
caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
// 提取文件名作为模块名
fileName := filepath.Base(file)
moduleName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
// 格式化消息
message := fmt.Sprintf(format, args...)
logMessage := fmt.Sprintf("[%s] [%s] %s", level.String(), caller, message)
// 添加调用位置信息
caller := fmt.Sprintf("%s:%d", fileName, line)
l.logger.Println(logMessage)
if l.config.StructuredLog {
// 结构化日志格式
entry := StructuredLogEntry{
Timestamp: GetCurrentTime(),
Level: level.String(),
Message: message,
Caller: caller,
Module: moduleName,
}
jsonBytes, err := json.Marshal(entry)
if err != nil {
// 如果JSON序列化失败回退到普通格式
fullMessage := fmt.Sprintf("[%s] [%s:%d] %s", level.String(), fileName, line, message)
l.logToLevel(level, fullMessage)
return
}
l.logToLevel(level, string(jsonBytes))
} else {
// 普通文本格式
fullMessage := fmt.Sprintf("[%s] [%s:%d] %s", level.String(), fileName, line, message)
l.logToLevel(level, fullMessage)
}
}
// logToLevel 根据级别输出日志
func (l *Logger) logToLevel(level LogLevel, message string) {
switch level {
case DEBUG:
l.debugLogger.Println(message)
case INFO:
l.infoLogger.Println(message)
case WARN:
l.warnLogger.Println(message)
case ERROR:
l.errorLogger.Println(message)
case FATAL:
l.fatalLogger.Println(message)
// Fatal级别终止程序
if level == FATAL {
os.Exit(1)
}
}
@@ -345,162 +139,72 @@ func (l *Logger) Fatal(format string, args ...interface{}) {
l.log(FATAL, format, args...)
}
// startRotationCheck 启动日志轮转检查
func (l *Logger) startRotationCheck() {
ticker := time.NewTicker(1 * time.Hour) // 每小时检查一次
defer ticker.Stop()
for range ticker.C {
l.checkRotation()
}
// TelegramDebug Telegram调试日志
func (l *Logger) TelegramDebug(format string, args ...interface{}) {
l.log(DEBUG, "[TELEGRAM] "+format, args...)
}
// checkRotation 检查是否需要轮转日志
func (l *Logger) checkRotation() {
if !l.config.EnableFile || l.file == nil {
return
}
// 检查文件大小
fileInfo, err := l.file.Stat()
if err != nil {
return
}
// 如果文件超过最大大小,进行轮转
if fileInfo.Size() > l.config.MaxFileSize*1024*1024 {
l.rotateLog()
}
// 清理旧日志文件
l.cleanOldLogs()
// TelegramInfo Telegram信息日志
func (l *Logger) TelegramInfo(format string, args ...interface{}) {
l.log(INFO, "[TELEGRAM] "+format, args...)
}
// rotateLog 轮转日志文件
func (l *Logger) rotateLog() {
// TelegramWarn Telegram警告日志
func (l *Logger) TelegramWarn(format string, args ...interface{}) {
l.log(WARN, "[TELEGRAM] "+format, args...)
}
// TelegramError Telegram错误日志
func (l *Logger) TelegramError(format string, args ...interface{}) {
l.log(ERROR, "[TELEGRAM] "+format, args...)
}
// DebugWithFields 带字段的调试日志
func (l *Logger) DebugWithFields(fields map[string]interface{}, format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if len(fields) > 0 {
var fieldStrs []string
for k, v := range fields {
fieldStrs = append(fieldStrs, fmt.Sprintf("%s=%v", k, v))
}
message = fmt.Sprintf("%s [%s]", message, strings.Join(fieldStrs, ", "))
}
l.log(DEBUG, message)
}
// InfoWithFields 带字段的信息日志
func (l *Logger) InfoWithFields(fields map[string]interface{}, format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if len(fields) > 0 {
var fieldStrs []string
for k, v := range fields {
fieldStrs = append(fieldStrs, fmt.Sprintf("%s=%v", k, v))
}
message = fmt.Sprintf("%s [%s]", message, strings.Join(fieldStrs, ", "))
}
l.log(INFO, message)
}
// ErrorWithFields 带字段的错误日志
func (l *Logger) ErrorWithFields(fields map[string]interface{}, format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
if len(fields) > 0 {
var fieldStrs []string
for k, v := range fields {
fieldStrs = append(fieldStrs, fmt.Sprintf("%s=%v", k, v))
}
message = fmt.Sprintf("%s [%s]", message, strings.Join(fieldStrs, ", "))
}
l.log(ERROR, message)
}
// Close 关闭日志文件
func (l *Logger) Close() {
l.mu.Lock()
defer l.mu.Unlock()
// 关闭当前文件
if l.file != nil {
l.file.Close()
}
// 重命名当前日志文件
currentLogFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s.log", GetCurrentTime().Format("2006-01-02")))
backupLogFile := filepath.Join(l.config.LogDir, fmt.Sprintf("app_%s_%s.log", GetCurrentTime().Format("2006-01-02"), GetCurrentTime().Format("15-04-05")))
if _, err := os.Stat(currentLogFile); err == nil {
os.Rename(currentLogFile, backupLogFile)
}
// 创建新的日志文件
l.initLogFile()
l.initLoggers()
}
// cleanOldLogs 清理旧日志文件
func (l *Logger) cleanOldLogs() {
if l.config.MaxAge <= 0 {
return
}
files, err := filepath.Glob(filepath.Join(l.config.LogDir, "app_*.log"))
if err != nil {
return
}
cutoffTime := GetCurrentTime().AddDate(0, 0, -l.config.MaxAge)
for _, file := range files {
fileInfo, err := os.Stat(file)
if err != nil {
continue
}
if fileInfo.ModTime().Before(cutoffTime) {
os.Remove(file)
}
}
}
// Min 返回两个整数中的较小值
func Min(a, b int) int {
if a < b {
return a
}
return b
}
// 结构化日志方法
func (l *Logger) DebugWithFields(fields map[string]interface{}, format string, args ...interface{}) {
l.logWithFields(DEBUG, fields, format, args...)
}
func (l *Logger) InfoWithFields(fields map[string]interface{}, format string, args ...interface{}) {
l.logWithFields(INFO, fields, format, args...)
}
func (l *Logger) WarnWithFields(fields map[string]interface{}, format string, args ...interface{}) {
l.logWithFields(WARN, fields, format, args...)
}
func (l *Logger) ErrorWithFields(fields map[string]interface{}, format string, args ...interface{}) {
l.logWithFields(ERROR, fields, format, args...)
}
func (l *Logger) FatalWithFields(fields map[string]interface{}, format string, args ...interface{}) {
l.logWithFields(FATAL, fields, format, args...)
}
// logWithFields 带字段的结构化日志方法
func (l *Logger) logWithFields(level LogLevel, fields map[string]interface{}, format string, args ...interface{}) {
if level < l.config.LogLevel {
return
}
// 获取调用者信息
_, file, line, ok := runtime.Caller(2)
if !ok {
file = "unknown"
line = 0
}
// 提取文件名作为模块名
fileName := filepath.Base(file)
moduleName := strings.TrimSuffix(fileName, filepath.Ext(fileName))
// 格式化消息
message := fmt.Sprintf(format, args...)
// 添加调用位置信息
caller := fmt.Sprintf("%s:%d", fileName, line)
if l.config.StructuredLog {
// 结构化日志格式
entry := StructuredLogEntry{
Timestamp: GetCurrentTime(),
Level: level.String(),
Message: message,
Caller: caller,
Module: moduleName,
Fields: fields,
}
jsonBytes, err := json.Marshal(entry)
if err != nil {
// 如果JSON序列化失败回退到普通格式
fullMessage := fmt.Sprintf("[%s] [%s:%d] %s - Fields: %v", level.String(), fileName, line, message, fields)
l.logToLevel(level, fullMessage)
return
}
l.logToLevel(level, string(jsonBytes))
} else {
// 普通文本格式
fullMessage := fmt.Sprintf("[%s] [%s:%d] %s - Fields: %v", level.String(), fileName, line, message, fields)
l.logToLevel(level, fullMessage)
}
}
// 全局便捷函数
@@ -524,7 +228,22 @@ func Fatal(format string, args ...interface{}) {
GetLogger().Fatal(format, args...)
}
// 全局结构化日志便捷函数
func TelegramDebug(format string, args ...interface{}) {
GetLogger().TelegramDebug(format, args...)
}
func TelegramInfo(format string, args ...interface{}) {
GetLogger().TelegramInfo(format, args...)
}
func TelegramWarn(format string, args ...interface{}) {
GetLogger().TelegramWarn(format, args...)
}
func TelegramError(format string, args ...interface{}) {
GetLogger().TelegramError(format, args...)
}
func DebugWithFields(fields map[string]interface{}, format string, args ...interface{}) {
GetLogger().DebugWithFields(fields, format, args...)
}
@@ -533,14 +252,15 @@ func InfoWithFields(fields map[string]interface{}, format string, args ...interf
GetLogger().InfoWithFields(fields, format, args...)
}
func WarnWithFields(fields map[string]interface{}, format string, args ...interface{}) {
GetLogger().WarnWithFields(fields, format, args...)
}
func ErrorWithFields(fields map[string]interface{}, format string, args ...interface{}) {
GetLogger().ErrorWithFields(fields, format, args...)
}
func FatalWithFields(fields map[string]interface{}, format string, args ...interface{}) {
GetLogger().FatalWithFields(fields, format, args...)
// Min 返回两个整数中的较小值
func Min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,456 @@
<template>
<div class="tab-content-container">
<div class="space-y-6">
<!-- 基础配置 -->
<n-card title="基础配置" class="mb-6">
<n-form :model="configForm" label-placement="left" label-width="120px">
<n-form-item label="AppID">
<n-input v-model:value="configForm.app_id" placeholder="请输入微信公众号AppID" />
</n-form-item>
<n-form-item label="AppSecret">
<n-input v-model:value="configForm.app_secret" type="password" placeholder="请输入AppSecret" show-password-on="click" />
</n-form-item>
<n-form-item label="Token">
<n-input v-model:value="configForm.token" placeholder="请输入Token用于消息验证" />
</n-form-item>
<n-form-item label="EncodingAESKey">
<n-input v-model:value="configForm.encoding_aes_key" type="password" placeholder="请输入EncodingAESKey可选用于消息加密" show-password-on="click" />
</n-form-item>
</n-form>
</n-card>
<!-- 功能配置 -->
<n-card title="功能配置" class="mb-6">
<n-form :model="configForm" label-placement="left" label-width="120px">
<n-form-item label="启用机器人">
<n-switch v-model:value="configForm.enabled" />
</n-form-item>
<n-form-item label="自动回复">
<n-switch v-model:value="configForm.auto_reply_enabled" />
</n-form-item>
<n-form-item label="欢迎消息">
<n-input v-model:value="configForm.welcome_message" type="textarea" :rows="3" placeholder="新用户关注时发送的欢迎消息" />
</n-form-item>
<n-form-item label="搜索结果限制">
<n-input-number v-model:value="configForm.search_limit" :min="1" :max="10" placeholder="搜索结果返回数量" />
</n-form-item>
</n-form>
</n-card>
<!-- 微信公众号验证文件上传 -->
<n-card title="微信公众号验证文件" class="mb-6">
<div class="space-y-4">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h4 class="font-medium text-blue-800 dark:text-blue-200 mb-2">验证文件上传说明</h4>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">
微信公众号需要上传一个TXT验证文件到网站根目录请按照以下步骤操作
</p>
<ol class="text-sm text-gray-700 dark:text-gray-300 list-decimal list-inside space-y-1">
<li>点击下方"选择文件"按钮选择微信提供的TXT验证文件</li>
<li>点击"上传验证文件"按钮上传文件</li>
<li>上传成功后文件将可通过网站根目录直接访问</li>
<li>在微信公众平台完成域名验证</li>
</ol>
</div>
<div class="flex items-center space-x-4">
<n-upload
ref="uploadRef"
:show-file-list="false"
:accept="'.txt'"
:max="1"
:custom-request="handleUpload"
@before-upload="beforeUpload"
@change="handleFileChange"
>
<n-button type="primary">
<template #icon>
<i class="fas fa-file-upload"></i>
</template>
选择TXT文件
</n-button>
</n-upload>
<n-button
type="success"
@click="triggerUpload"
:disabled="!selectedFile"
:loading="uploading"
>
<template #icon>
<i class="fas fa-cloud-upload-alt"></i>
</template>
上传验证文件
</n-button>
</div>
<div v-if="uploadResult" class="p-3 rounded-md bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300">
<div class="flex items-center">
<i class="fas fa-check-circle mr-2"></i>
<span>文件上传成功</span>
</div>
<p class="text-xs mt-1">文件名: {{ uploadResult.file_name }}</p>
<p class="text-xs">访问地址: {{ getFullUrl(uploadResult.access_url) }}</p>
</div>
</div>
</n-card>
<!-- 微信公众号平台配置说明 -->
<n-card title="微信公众号平台配置" class="mb-6">
<div class="space-y-4">
<div class="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-4">
<h4 class="font-medium text-blue-800 dark:text-blue-200 mb-2">服务器配置</h4>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-2">
在微信公众平台后台的<strong>开发 > 基本配置 > 服务器配置</strong>中设置
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 block">URL</label>
<div class="flex items-center space-x-2">
<n-input :value="serverUrl" readonly class="flex-1" />
<n-button size="small" @click="copyToClipboard(serverUrl)" type="primary">
<template #icon>
<i class="fas fa-copy"></i>
</template>
复制
</n-button>
</div>
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1 block">Token</label>
<div class="flex items-center space-x-2">
<n-input :value="configForm.token" readonly class="flex-1" />
<n-button size="small" @click="copyToClipboard(configForm.token)" type="primary">
<template #icon>
<i class="fas fa-copy"></i>
</template>
复制
</n-button>
</div>
</div>
</div>
</div>
<div class="bg-green-50 dark:bg-green-900/20 rounded-lg p-4">
<h4 class="font-medium text-green-800 dark:text-green-200 mb-2">消息加解密配置</h4>
<p class="text-sm text-gray-700 dark:text-gray-300">
如果需要启用消息加密请在微信公众平台选择<strong>安全模式</strong>并填写上面的EncodingAESKey
</p>
</div>
<div class="bg-yellow-50 dark:bg-yellow-900/20 rounded-lg p-4">
<h4 class="font-medium text-yellow-800 dark:text-yellow-200 mb-2">注意事项</h4>
<ul class="text-sm text-gray-700 dark:text-gray-300 list-disc list-inside space-y-1">
<li>服务器必须支持HTTPS微信要求</li>
<li>域名必须已备案</li>
<li>首次配置时微信会发送GET请求验证服务器</li>
<li>配置完成后记得点击"启用"按钮</li>
</ul>
</div>
</div>
</n-card>
<!-- 操作按钮 -->
<div class="flex justify-end space-x-4">
<n-button @click="resetForm">重置</n-button>
<n-button type="primary" @click="saveConfig" :loading="loading">保存配置</n-button>
</div>
<!-- 状态信息 -->
<n-card title="运行状态" class="mt-6">
<div class="space-y-4">
<div class="flex items-center space-x-4">
<n-tag :type="botStatus.overall_status ? 'success' : 'default'">
{{ botStatus.status_text || '未知状态' }}
</n-tag>
<n-tag v-if="botStatus.config" :type="botStatus.config.enabled ? 'success' : 'default'">
配置状态: {{ botStatus.config.enabled ? '已启用' : '已禁用' }}
</n-tag>
<n-tag v-if="botStatus.config" :type="botStatus.config.app_id_configured ? 'success' : 'warning'">
AppID: {{ botStatus.config.app_id_configured ? '已配置' : '未配置' }}
</n-tag>
</div>
<div v-if="!botStatus.overall_status && botStatus.config && botStatus.config.enabled" class="bg-orange-50 dark:bg-orange-900/20 rounded-lg p-3">
<p class="text-sm text-orange-800 dark:text-orange-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
机器人已启用但未运行请检查配置是否正确或查看系统日志
</p>
</div>
</div>
</n-card>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { useNotification } from 'naive-ui'
import { useWechatApi } from '~/composables/useApi'
// 定义配置表单类型
interface WechatBotConfigForm {
enabled: boolean
app_id: string
app_secret: string
token: string
encoding_aes_key: string
welcome_message: string
auto_reply_enabled: boolean
search_limit: number
}
const notification = useNotification()
const loading = ref(false)
const wechatApi = useWechatApi()
const botStatus = ref({
overall_status: false,
status_text: '',
config: null as any,
runtime: null as any
})
// 验证文件上传相关
const uploadRef = ref()
const selectedFile = ref<File | null>(null)
const uploading = ref(false)
const uploadResult = ref<any>(null)
// 配置表单 - 直接使用 reactive
const configForm = reactive<WechatBotConfigForm>({
enabled: false,
app_id: '',
app_secret: '',
token: '',
encoding_aes_key: '',
welcome_message: '欢迎关注老九网盘资源库!发送关键词即可搜索资源。',
auto_reply_enabled: true,
search_limit: 5
})
// 计算服务器URL
const serverUrl = computed(() => {
if (process.client) {
return `${window.location.origin}/api/wechat/callback`
}
return 'https://yourdomain.com/api/wechat/callback'
})
// 获取机器人配置
const fetchBotConfig = async () => {
try {
const response = await wechatApi.getBotConfig()
if (response) {
// 直接更新 configForm
configForm.enabled = response.enabled || false
configForm.app_id = response.app_id || ''
configForm.app_secret = response.app_secret || '' // 现在所有字段都不敏感
configForm.token = response.token || ''
configForm.encoding_aes_key = response.encoding_aes_key || ''
configForm.welcome_message = response.welcome_message || '欢迎关注老九网盘资源库!发送关键词即可搜索资源。'
configForm.auto_reply_enabled = response.auto_reply_enabled || true
configForm.search_limit = response.search_limit || 5
}
} catch (error) {
console.error('获取微信机器人配置失败:', error)
notification.error({
content: '获取配置失败',
duration: 3000
})
}
}
// 获取机器人状态
const fetchBotStatus = async () => {
try {
const response = await wechatApi.getBotStatus()
if (response) {
botStatus.value = response
}
} catch (error) {
console.error('获取微信机器人状态失败:', error)
notification.error({
content: '获取状态失败',
duration: 3000
})
}
}
// 保存配置
const saveConfig = async () => {
loading.value = true
try {
// 直接保存所有字段,不检测变更
const payload = {
enabled: configForm.enabled,
app_id: configForm.app_id,
app_secret: configForm.app_secret,
token: configForm.token,
encoding_aes_key: configForm.encoding_aes_key,
welcome_message: configForm.welcome_message,
auto_reply_enabled: configForm.auto_reply_enabled,
search_limit: configForm.search_limit
}
const response = await wechatApi.updateBotConfig(payload)
if (response.success) {
notification.success({
content: '配置保存成功',
duration: 3000
})
// 重新获取状态和配置
await fetchBotConfig()
await fetchBotStatus()
} else {
throw new Error(response.message || '保存失败')
}
} catch (error: any) {
console.error('保存微信机器人配置失败:', error)
notification.error({
content: error.message || '保存配置失败',
duration: 3000
})
} finally {
loading.value = false
}
}
// 重置表单
const resetForm = () => {
// 重新获取原始配置
fetchBotConfig()
}
// 复制到剪贴板
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
notification.success({
content: '已复制到剪贴板',
duration: 2000
})
} catch (error) {
console.error('复制失败:', error)
notification.error({
content: '复制失败',
duration: 2000
})
}
}
// 验证文件上传相关函数
const beforeUpload = (options: { file: any, fileList: any[] }) => {
// 从 options 中提取文件
const file = options?.file?.file || options?.file
// 检查文件对象是否有效
if (!file || !file.name) {
notification.error({
content: '文件选择失败,请重试',
duration: 2000
})
return false
}
// 验证文件类型 - 使用多重检查确保是TXT文件
const isValid = file.type === 'text/plain' ||
file.name.toLowerCase().endsWith('.txt') ||
file.type === 'text/plain;charset=utf-8'
if (!isValid) {
notification.error({
content: '请上传TXT文件',
duration: 2000
})
selectedFile.value = null // 清空之前的选择
return false // 阻止上传无效文件
}
// 保存选中的文件并更新状态
selectedFile.value = file
return false // 阻止自动上传
}
const handleUpload = ({ file, onSuccess, onError }: any) => {
// 这个函数不会被调用,因为我们阻止了自动上传
}
// 文件选择变化时的处理函数
const handleFileChange = (options: { file: any, fileList: any[] }) => {
// 从 change 事件中提取文件信息
const file = options?.file?.file || options?.file
if (file && file.name) {
// 更新选中的文件
selectedFile.value = file
}
}
const triggerUpload = async () => {
if (!selectedFile.value) {
notification.warning({
content: '请选择要上传的TXT文件',
duration: 2000
})
return
}
uploading.value = true
try {
const formData = new FormData()
formData.append('file', selectedFile.value)
const response = await wechatApi.uploadVerifyFile(formData)
if (response.success) {
uploadResult.value = response
notification.success({
content: '验证文件上传成功',
duration: 3000
})
// 清空选择的文件
selectedFile.value = null
if (uploadRef.value) {
uploadRef.value.clear()
}
} else {
throw new Error(response.message || '上传失败')
}
} catch (error: any) {
console.error('上传验证文件失败:', error)
notification.error({
content: error.message || '上传验证文件失败',
duration: 3000
})
} finally {
uploading.value = false
}
}
const getFullUrl = (path: string) => {
if (process.client) {
return `${window.location.origin}${path}`
}
return path
}
// 页面加载时获取配置和状态
onMounted(async () => {
await fetchBotConfig()
await fetchBotStatus()
// 定期刷新状态
const interval = setInterval(fetchBotStatus, 30000)
onUnmounted(() => clearInterval(interval))
})
</script>
<style scoped>
/* 微信公众号机器人标签样式 */
.tab-content-container {
height: calc(100vh - 240px);
overflow-y: auto;
padding-bottom: 1rem;
}
</style>

View File

@@ -96,7 +96,8 @@ export const useCksApi = () => {
const updateCks = (id: number, data: any) => useApiFetch(`/cks/${id}`, { method: 'PUT', body: data }).then(parseApiResponse)
const deleteCks = (id: number) => useApiFetch(`/cks/${id}`, { method: 'DELETE' }).then(parseApiResponse)
const refreshCapacity = (id: number) => useApiFetch(`/cks/${id}/refresh-capacity`, { method: 'POST' }).then(parseApiResponse)
return { getCks, getCksByID, createCks, updateCks, deleteCks, refreshCapacity }
const deleteRelatedResources = (id: number) => useApiFetch(`/cks/${id}/delete-related-resources`, { method: 'POST' }).then(parseApiResponse)
return { getCks, getCksByID, createCks, updateCks, deleteCks, refreshCapacity, deleteRelatedResources }
}
export const useTagApi = () => {
@@ -354,4 +355,18 @@ export const useSystemLogApi = () => {
getSystemLogSummary,
clearSystemLogs
}
}
// 微信机器人管理API
export const useWechatApi = () => {
const getBotConfig = () => useApiFetch('/wechat/bot-config').then(parseApiResponse)
const updateBotConfig = (data: any) => useApiFetch('/wechat/bot-config', { method: 'PUT', body: data }).then(parseApiResponse)
const getBotStatus = () => useApiFetch('/wechat/bot-status').then(parseApiResponse)
const uploadVerifyFile = (formData: FormData) => useApiFetch('/wechat/verify-file', { method: 'POST', body: formData }).then(parseApiResponse)
return {
getBotConfig,
updateBotConfig,
getBotStatus,
uploadVerifyFile
}
}

View File

@@ -18,7 +18,7 @@ interface VersionResponse {
export const useVersion = () => {
const versionInfo = ref<VersionInfo>({
version: '1.3.2',
version: '1.3.4',
build_time: '',
git_commit: 'unknown',
git_branch: 'unknown',

View File

@@ -64,10 +64,11 @@ const injectRawScript = (rawScriptString: string) => {
const container = document.createElement('div');
container.innerHTML = rawScriptString.trim();
// 获取解析后的 script 元素
const script = container.querySelector('script');
// 获取解析后的所有 script 元素
const scripts = container.querySelectorAll('script');
if (script) {
// 遍历并注入所有脚本
scripts.forEach((script) => {
// 创建新的 script 元素
const newScript = document.createElement('script');
@@ -83,7 +84,7 @@ const injectRawScript = (rawScriptString: string) => {
// 插入到 DOM
document.head.appendChild(newScript);
}
});
}
};

View File

@@ -1,6 +1,6 @@
{
"name": "res-db-web",
"version": "1.3.2",
"version": "1.3.4",
"private": true,
"type": "module",
"scripts": {

View File

@@ -133,6 +133,9 @@
<span class="text-xs text-gray-500 dark:text-gray-400">
剩余: {{ formatFileSize(Math.max(0, item.left_space)) }}
</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
已转存: {{ item.transferred_count || 0 }}
</span>
</div>
<!-- 备注 -->
@@ -146,25 +149,20 @@
<!-- 右侧操作按钮 -->
<div class="flex items-center space-x-2 ml-4">
<n-button size="small" :type="item.is_valid ? 'warning' : 'success'" @click="toggleStatus(item)"
:title="item.is_valid ? '禁用账号' : '启用账号'">
<template #icon>
<i :class="item.is_valid ? 'fas fa-ban' : 'fas fa-check'"></i>
</template>
:title="item.is_valid ? '禁用账号' : '启用账号'" text>
{{ item.is_valid ? '禁用' : '启用' }}
</n-button>
<n-button size="small" type="info" @click="refreshCapacity(item.id)" title="刷新容量">
<template #icon>
<i class="fas fa-sync-alt"></i>
</template>
<n-button size="small" type="info" @click="refreshCapacity(item.id)" title="刷新容量" text>
刷新容量
</n-button>
<n-button size="small" type="primary" @click="editCks(item)" title="编辑账号">
<template #icon>
<i class="fas fa-edit"></i>
</template>
<n-button size="small" type="primary" @click="editCks(item)" title="编辑账号" text>
编辑
</n-button>
<n-button size="small" type="error" @click="deleteCks(item.id)" title="删除账号">
<template #icon>
<i class="fas fa-trash"></i>
</template>
<n-button size="small" type="error" @click="deleteCks(item.id)" title="删除账号" text>
删除
</n-button>
<n-button size="small" type="warning" @click="deleteRelatedResources(item.id)" title="删除关联资源" text>
删除关联
</n-button>
</div>
</div>
@@ -241,9 +239,15 @@
<template #footer>
<div class="flex justify-end space-x-3">
<n-button type="tertiary" @click="closeModal">
<template #icon>
<i class="fas fa-times"></i>
</template>
取消
</n-button>
<n-button type="primary" :loading="submitting" @click="handleSubmit">
<template #icon>
<i class="fas fa-check"></i>
</template>
{{ showEditModal ? '更新' : '创建' }}
</n-button>
</div>
@@ -428,6 +432,36 @@ const deleteCks = async (id) => {
})
}
// 删除关联资源
const deleteRelatedResources = async (id) => {
dialog.warning({
title: '警告',
content: '确定要删除与此账号关联的所有资源吗?这将清空这些资源的转存信息,变为未转存状态。',
positiveText: '确定',
negativeText: '取消',
draggable: true,
onPositiveClick: async () => {
try {
// 调用API删除关联资源
await cksApi.deleteRelatedResources(id)
await fetchCks()
notification.success({
title: '成功',
content: '关联资源已删除!',
duration: 3000
})
} catch (error) {
console.error('删除关联资源失败:', error)
notification.error({
title: '失败',
content: '删除关联资源失败: ' + (error.message || '未知错误'),
duration: 3000
})
}
}
})
}
// 刷新容量
const refreshCapacity = async (id) => {
dialog.warning({

View File

@@ -23,13 +23,7 @@
</n-tab-pane>
<n-tab-pane name="wechat" tab="微信公众号">
<div class="tab-content-container">
<div class="text-center py-12">
<i class="fas fa-lock text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">功能暂未开放</h3>
<p class="text-gray-500 dark:text-gray-400">微信公众号机器人功能正在开发中敬请期待</p>
</div>
</div>
<WechatBotTab />
</n-tab-pane>
<n-tab-pane name="telegram" tab="Telegram机器人">
@@ -59,6 +53,7 @@ import { ref } from 'vue'
import AdminPageLayout from '~/components/AdminPageLayout.vue'
import TelegramBotTab from '~/components/TelegramBotTab.vue'
import QqBotTab from '~/components/QqBotTab.vue'
import WechatBotTab from '~/components/WechatBotTab.vue'
// 设置页面布局
definePageMeta({

View File

@@ -150,6 +150,10 @@
<i class="fas fa-eye mr-1"></i>
{{ resource.view_count || 0 }}
</span>
<span>
<i class="fas fa-clock mr-1"></i>
{{ resource.updated_at }}
</span>
</div>
<div v-if="resource.tags && resource.tags.length > 0" class="mt-2">
@@ -670,20 +674,20 @@ const handleEditSubmit = async () => {
try {
editing.value = true
await editFormRef.value?.validate()
await resourceApi.updateResource(editingResource.value!.id, editForm.value)
notification.success({
content: '更新成功',
duration: 3000
})
// 更新本地数据
const resourceId = editingResource.value?.id
const index = resources.value.findIndex(r => r.id === resourceId)
if (index > -1) {
resources.value[index] = {
...resources.value[index],
resources.value[index] = {
...resources.value[index],
title: editForm.value.title,
description: editForm.value.description,
url: editForm.value.url,
@@ -692,7 +696,7 @@ const handleEditSubmit = async () => {
tag_ids: editForm.value.tag_ids
}
}
showEditModal.value = false
editingResource.value = null
} catch (error) {

View File

@@ -14,13 +14,13 @@ export const useSystemConfigStore = defineStore('systemConfig', {
// 根据上下文选择API管理员页面使用管理员API其他页面使用公开API
const apiUrl = useAdminApi ? '/system/config' : '/public/system-config'
const response = await useApiFetch(apiUrl)
console.log('Store API响应:', response) // 调试信息
// console.log('Store API响应:', response) // 调试信息
// 使用parseApiResponse正确解析API响应
const data = parseApiResponse(response)
console.log('Store 处理后的数据:', data) // 调试信息
console.log('Store 自动处理状态:', data.auto_process_ready_resources)
console.log('Store 自动转存状态:', data.auto_transfer_enabled)
// console.log('Store 处理后的数据:', data) // 调试信息
// console.log('Store 自动处理状态:', data.auto_process_ready_resources)
// console.log('Store 自动转存状态:', data.auto_transfer_enabled)
this.config = data
this.initialized = true