mirror of
https://github.com/ctwj/urldb.git
synced 2025-11-25 03:15:04 +08:00
feat: 新增搜索增加
This commit is contained in:
@@ -263,6 +263,13 @@ func insertDefaultDataIfEmpty() error {
|
||||
{Key: entity.ConfigKeyAutoInsertAd, Value: entity.ConfigDefaultAutoInsertAd, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchEnabled, Value: entity.ConfigDefaultMeilisearchEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyMeilisearchHost, Value: entity.ConfigDefaultMeilisearchHost, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
for _, config := range defaultSystemConfigs {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
)
|
||||
@@ -10,22 +11,24 @@ import (
|
||||
// ToResourceResponse 将Resource实体转换为ResourceResponse
|
||||
func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
response := dto.ResourceResponse{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
PanID: resource.PanID,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
CategoryID: resource.CategoryID,
|
||||
ViewCount: resource.ViewCount,
|
||||
IsValid: resource.IsValid,
|
||||
IsPublic: resource.IsPublic,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
Cover: resource.Cover,
|
||||
Author: resource.Author,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
PanID: resource.PanID,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
CategoryID: resource.CategoryID,
|
||||
ViewCount: resource.ViewCount,
|
||||
IsValid: resource.IsValid,
|
||||
IsPublic: resource.IsPublic,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
Cover: resource.Cover,
|
||||
Author: resource.Author,
|
||||
ErrorMsg: resource.ErrorMsg,
|
||||
SyncedToMeilisearch: resource.SyncedToMeilisearch,
|
||||
SyncedAt: resource.SyncedAt,
|
||||
}
|
||||
|
||||
// 设置分类名称
|
||||
@@ -47,6 +50,89 @@ func ToResourceResponse(resource *entity.Resource) dto.ResourceResponse {
|
||||
return response
|
||||
}
|
||||
|
||||
// ToResourceResponseFromMeilisearch 将MeilisearchDocument转换为ResourceResponse(包含高亮信息)
|
||||
func ToResourceResponseFromMeilisearch(doc interface{}) dto.ResourceResponse {
|
||||
// 使用反射来获取MeilisearchDocument的字段
|
||||
docValue := reflect.ValueOf(doc)
|
||||
if docValue.Kind() == reflect.Ptr {
|
||||
docValue = docValue.Elem()
|
||||
}
|
||||
|
||||
response := dto.ResourceResponse{}
|
||||
|
||||
// 获取基本字段
|
||||
if idField := docValue.FieldByName("ID"); idField.IsValid() {
|
||||
response.ID = uint(idField.Uint())
|
||||
}
|
||||
if titleField := docValue.FieldByName("Title"); titleField.IsValid() {
|
||||
response.Title = titleField.String()
|
||||
}
|
||||
if descField := docValue.FieldByName("Description"); descField.IsValid() {
|
||||
response.Description = descField.String()
|
||||
}
|
||||
if urlField := docValue.FieldByName("URL"); urlField.IsValid() {
|
||||
response.URL = urlField.String()
|
||||
}
|
||||
if saveURLField := docValue.FieldByName("SaveURL"); saveURLField.IsValid() {
|
||||
response.SaveURL = saveURLField.String()
|
||||
}
|
||||
if fileSizeField := docValue.FieldByName("FileSize"); fileSizeField.IsValid() {
|
||||
response.FileSize = fileSizeField.String()
|
||||
}
|
||||
if keyField := docValue.FieldByName("Key"); keyField.IsValid() {
|
||||
// Key字段在ResourceResponse中不存在,跳过
|
||||
}
|
||||
if categoryField := docValue.FieldByName("Category"); categoryField.IsValid() {
|
||||
response.CategoryName = categoryField.String()
|
||||
}
|
||||
if authorField := docValue.FieldByName("Author"); authorField.IsValid() {
|
||||
response.Author = authorField.String()
|
||||
}
|
||||
if createdAtField := docValue.FieldByName("CreatedAt"); createdAtField.IsValid() {
|
||||
response.CreatedAt = createdAtField.Interface().(time.Time)
|
||||
}
|
||||
if updatedAtField := docValue.FieldByName("UpdatedAt"); updatedAtField.IsValid() {
|
||||
response.UpdatedAt = updatedAtField.Interface().(time.Time)
|
||||
}
|
||||
|
||||
// 处理PanID
|
||||
if panIDField := docValue.FieldByName("PanID"); panIDField.IsValid() && !panIDField.IsNil() {
|
||||
panIDPtr := panIDField.Interface().(*uint)
|
||||
if panIDPtr != nil {
|
||||
response.PanID = panIDPtr
|
||||
}
|
||||
}
|
||||
|
||||
// 处理Tags
|
||||
if tagsField := docValue.FieldByName("Tags"); tagsField.IsValid() {
|
||||
tags := tagsField.Interface().([]string)
|
||||
response.Tags = make([]dto.TagResponse, len(tags))
|
||||
for i, tagName := range tags {
|
||||
response.Tags[i] = dto.TagResponse{
|
||||
Name: tagName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理高亮字段
|
||||
if titleHighlightField := docValue.FieldByName("TitleHighlight"); titleHighlightField.IsValid() {
|
||||
response.TitleHighlight = titleHighlightField.String()
|
||||
}
|
||||
if descHighlightField := docValue.FieldByName("DescriptionHighlight"); descHighlightField.IsValid() {
|
||||
response.DescriptionHighlight = descHighlightField.String()
|
||||
}
|
||||
if categoryHighlightField := docValue.FieldByName("CategoryHighlight"); categoryHighlightField.IsValid() {
|
||||
response.CategoryHighlight = categoryHighlightField.String()
|
||||
}
|
||||
if tagsHighlightField := docValue.FieldByName("TagsHighlight"); tagsHighlightField.IsValid() {
|
||||
tagsHighlight := tagsHighlightField.Interface().([]string)
|
||||
response.TagsHighlight = make([]string, len(tagsHighlight))
|
||||
copy(response.TagsHighlight, tagsHighlight)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ToResourceResponseList 将Resource实体列表转换为ResourceResponse列表
|
||||
func ToResourceResponseList(resources []entity.Resource) []dto.ResourceResponse {
|
||||
responses := make([]dto.ResourceResponse, len(resources))
|
||||
@@ -176,7 +262,7 @@ func ToReadyResourceResponse(resource *entity.ReadyResource) dto.ReadyResourceRe
|
||||
if isDeleted {
|
||||
deletedAt = &resource.DeletedAt.Time
|
||||
}
|
||||
|
||||
|
||||
return dto.ReadyResourceResponse{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
|
||||
@@ -78,6 +78,18 @@ func SystemConfigToResponse(configs []entity.SystemConfig) *dto.SystemConfigResp
|
||||
}
|
||||
case entity.ConfigKeyThirdPartyStatsCode:
|
||||
response.ThirdPartyStatsCode = config.Value
|
||||
case entity.ConfigKeyMeilisearchEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response.MeilisearchEnabled = val
|
||||
}
|
||||
case entity.ConfigKeyMeilisearchHost:
|
||||
response.MeilisearchHost = config.Value
|
||||
case entity.ConfigKeyMeilisearchPort:
|
||||
response.MeilisearchPort = config.Value
|
||||
case entity.ConfigKeyMeilisearchMasterKey:
|
||||
response.MeilisearchMasterKey = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
response.MeilisearchIndexName = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +199,28 @@ func RequestToSystemConfig(req *dto.SystemConfigRequest) []entity.SystemConfig {
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyThirdPartyStatsCode)
|
||||
}
|
||||
|
||||
// Meilisearch配置 - 只处理被设置的字段
|
||||
if req.MeilisearchEnabled != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchEnabled, Value: strconv.FormatBool(*req.MeilisearchEnabled), Type: entity.ConfigTypeBool})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchEnabled)
|
||||
}
|
||||
if req.MeilisearchHost != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchHost, Value: *req.MeilisearchHost, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchHost)
|
||||
}
|
||||
if req.MeilisearchPort != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchPort, Value: *req.MeilisearchPort, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchPort)
|
||||
}
|
||||
if req.MeilisearchMasterKey != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchMasterKey, Value: *req.MeilisearchMasterKey, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchMasterKey)
|
||||
}
|
||||
if req.MeilisearchIndexName != nil {
|
||||
configs = append(configs, entity.SystemConfig{Key: entity.ConfigKeyMeilisearchIndexName, Value: *req.MeilisearchIndexName, Type: entity.ConfigTypeString})
|
||||
updatedKeys = append(updatedKeys, entity.ConfigKeyMeilisearchIndexName)
|
||||
}
|
||||
|
||||
// 记录更新的配置项
|
||||
if len(updatedKeys) > 0 {
|
||||
utils.Info("配置更新 - 被修改的配置项: %v", updatedKeys)
|
||||
@@ -219,6 +253,12 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
entity.ConfigResponseFieldPageSize: 100,
|
||||
entity.ConfigResponseFieldMaintenanceMode: false,
|
||||
entity.ConfigResponseFieldEnableRegister: true, // 默认开启注册功能
|
||||
entity.ConfigResponseFieldThirdPartyStatsCode: "",
|
||||
entity.ConfigResponseFieldMeilisearchEnabled: false,
|
||||
entity.ConfigResponseFieldMeilisearchHost: "localhost",
|
||||
entity.ConfigResponseFieldMeilisearchPort: "7700",
|
||||
entity.ConfigResponseFieldMeilisearchMasterKey: "",
|
||||
entity.ConfigResponseFieldMeilisearchIndexName: "resources",
|
||||
}
|
||||
|
||||
// 将键值对转换为map
|
||||
@@ -280,6 +320,18 @@ func SystemConfigToPublicResponse(configs []entity.SystemConfig) map[string]inte
|
||||
}
|
||||
case entity.ConfigKeyThirdPartyStatsCode:
|
||||
response[entity.ConfigResponseFieldThirdPartyStatsCode] = config.Value
|
||||
case entity.ConfigKeyMeilisearchEnabled:
|
||||
if val, err := strconv.ParseBool(config.Value); err == nil {
|
||||
response[entity.ConfigResponseFieldMeilisearchEnabled] = val
|
||||
}
|
||||
case entity.ConfigKeyMeilisearchHost:
|
||||
response[entity.ConfigResponseFieldMeilisearchHost] = config.Value
|
||||
case entity.ConfigKeyMeilisearchPort:
|
||||
response[entity.ConfigResponseFieldMeilisearchPort] = config.Value
|
||||
case entity.ConfigKeyMeilisearchMasterKey:
|
||||
response[entity.ConfigResponseFieldMeilisearchMasterKey] = config.Value
|
||||
case entity.ConfigKeyMeilisearchIndexName:
|
||||
response[entity.ConfigResponseFieldMeilisearchIndexName] = config.Value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,5 +367,10 @@ func getDefaultConfigResponse() *dto.SystemConfigResponse {
|
||||
MaintenanceMode: false,
|
||||
EnableRegister: true, // 默认开启注册功能
|
||||
ThirdPartyStatsCode: entity.ConfigDefaultThirdPartyStatsCode,
|
||||
MeilisearchEnabled: false,
|
||||
MeilisearchHost: entity.ConfigDefaultMeilisearchHost,
|
||||
MeilisearchPort: entity.ConfigDefaultMeilisearchPort,
|
||||
MeilisearchMasterKey: entity.ConfigDefaultMeilisearchMasterKey,
|
||||
MeilisearchIndexName: entity.ConfigDefaultMeilisearchIndexName,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,24 +12,31 @@ type SearchResponse struct {
|
||||
|
||||
// ResourceResponse 资源响应
|
||||
type ResourceResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
PanID *uint `json:"pan_id"`
|
||||
SaveURL string `json:"save_url"`
|
||||
FileSize string `json:"file_size"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
ViewCount int `json:"view_count"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
PanID *uint `json:"pan_id"`
|
||||
SaveURL string `json:"save_url"`
|
||||
FileSize string `json:"file_size"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
CategoryName string `json:"category_name"`
|
||||
ViewCount int `json:"view_count"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Cover string `json:"cover"`
|
||||
Author string `json:"author"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
SyncedToMeilisearch bool `json:"synced_to_meilisearch"`
|
||||
SyncedAt *time.Time `json:"synced_at"`
|
||||
// 高亮字段
|
||||
TitleHighlight string `json:"title_highlight,omitempty"`
|
||||
DescriptionHighlight string `json:"description_highlight,omitempty"`
|
||||
CategoryHighlight string `json:"category_highlight,omitempty"`
|
||||
TagsHighlight []string `json:"tags_highlight,omitempty"`
|
||||
}
|
||||
|
||||
// CategoryResponse 分类响应
|
||||
|
||||
@@ -35,6 +35,13 @@ type SystemConfigRequest struct {
|
||||
|
||||
// 三方统计配置
|
||||
ThirdPartyStatsCode *string `json:"third_party_stats_code,omitempty"` // 三方统计代码
|
||||
|
||||
// Meilisearch配置
|
||||
MeilisearchEnabled *bool `json:"meilisearch_enabled,omitempty"`
|
||||
MeilisearchHost *string `json:"meilisearch_host,omitempty"`
|
||||
MeilisearchPort *string `json:"meilisearch_port,omitempty"`
|
||||
MeilisearchMasterKey *string `json:"meilisearch_master_key,omitempty"`
|
||||
MeilisearchIndexName *string `json:"meilisearch_index_name,omitempty"`
|
||||
}
|
||||
|
||||
// SystemConfigResponse 系统配置响应
|
||||
@@ -76,6 +83,13 @@ type SystemConfigResponse struct {
|
||||
|
||||
// 三方统计配置
|
||||
ThirdPartyStatsCode string `json:"third_party_stats_code"` // 三方统计代码
|
||||
|
||||
// Meilisearch配置
|
||||
MeilisearchEnabled bool `json:"meilisearch_enabled"`
|
||||
MeilisearchHost string `json:"meilisearch_host"`
|
||||
MeilisearchPort string `json:"meilisearch_port"`
|
||||
MeilisearchMasterKey string `json:"meilisearch_master_key"`
|
||||
MeilisearchIndexName string `json:"meilisearch_index_name"`
|
||||
}
|
||||
|
||||
// SystemConfigItem 单个配置项
|
||||
|
||||
@@ -8,26 +8,28 @@ import (
|
||||
|
||||
// Resource 资源模型
|
||||
type Resource struct {
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Title string `json:"title" gorm:"size:255;not null;comment:资源标题"`
|
||||
Description string `json:"description" gorm:"type:text;comment:资源描述"`
|
||||
URL string `json:"url" gorm:"size:128;comment:资源链接"`
|
||||
PanID *uint `json:"pan_id" gorm:"comment:平台ID"`
|
||||
SaveURL string `json:"save_url" gorm:"size:500;comment:转存后的链接"`
|
||||
FileSize string `json:"file_size" gorm:"size:100;comment:文件大小"`
|
||||
CategoryID *uint `json:"category_id" gorm:"comment:分类ID"`
|
||||
ViewCount int `json:"view_count" gorm:"default:0;comment:浏览次数"`
|
||||
IsValid bool `json:"is_valid" gorm:"default:true;comment:是否有效"`
|
||||
IsPublic bool `json:"is_public" gorm:"default:true;comment:是否公开"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
Cover string `json:"cover" gorm:"size:500;comment:封面"`
|
||||
Author string `json:"author" gorm:"size:100;comment:作者"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"size:255;comment:转存失败原因"`
|
||||
CkID *uint `json:"ck_id" gorm:"comment:账号ID"`
|
||||
Fid string `json:"fid" gorm:"size:128;comment:网盘文件ID"`
|
||||
Key string `json:"key" gorm:"size:64;index;comment:资源组标识,相同key表示同一组资源"`
|
||||
ID uint `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Title string `json:"title" gorm:"size:255;not null;comment:资源标题"`
|
||||
Description string `json:"description" gorm:"type:text;comment:资源描述"`
|
||||
URL string `json:"url" gorm:"size:128;comment:资源链接"`
|
||||
PanID *uint `json:"pan_id" gorm:"comment:平台ID"`
|
||||
SaveURL string `json:"save_url" gorm:"size:500;comment:转存后的链接"`
|
||||
FileSize string `json:"file_size" gorm:"size:100;comment:文件大小"`
|
||||
CategoryID *uint `json:"category_id" gorm:"comment:分类ID"`
|
||||
ViewCount int `json:"view_count" gorm:"default:0;comment:浏览次数"`
|
||||
IsValid bool `json:"is_valid" gorm:"default:true;comment:是否有效"`
|
||||
IsPublic bool `json:"is_public" gorm:"default:true;comment:是否公开"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
Cover string `json:"cover" gorm:"size:500;comment:封面"`
|
||||
Author string `json:"author" gorm:"size:100;comment:作者"`
|
||||
ErrorMsg string `json:"error_msg" gorm:"size:255;comment:转存失败原因"`
|
||||
CkID *uint `json:"ck_id" gorm:"comment:账号ID"`
|
||||
Fid string `json:"fid" gorm:"size:128;comment:网盘文件ID"`
|
||||
Key string `json:"key" gorm:"size:64;index;comment:资源组标识,相同key表示同一组资源"`
|
||||
SyncedToMeilisearch bool `json:"synced_to_meilisearch" gorm:"default:false;comment:是否已同步到Meilisearch"`
|
||||
SyncedAt *time.Time `json:"synced_at" gorm:"comment:同步时间"`
|
||||
|
||||
// 关联关系
|
||||
Category Category `json:"category" gorm:"foreignKey:CategoryID"`
|
||||
|
||||
@@ -35,6 +35,13 @@ const (
|
||||
|
||||
// 三方统计配置
|
||||
ConfigKeyThirdPartyStatsCode = "third_party_stats_code"
|
||||
|
||||
// Meilisearch配置
|
||||
ConfigKeyMeilisearchEnabled = "meilisearch_enabled"
|
||||
ConfigKeyMeilisearchHost = "meilisearch_host"
|
||||
ConfigKeyMeilisearchPort = "meilisearch_port"
|
||||
ConfigKeyMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigKeyMeilisearchIndexName = "meilisearch_index_name"
|
||||
)
|
||||
|
||||
// ConfigType 配置类型常量
|
||||
@@ -84,6 +91,13 @@ const (
|
||||
|
||||
// 三方统计配置字段
|
||||
ConfigResponseFieldThirdPartyStatsCode = "third_party_stats_code"
|
||||
|
||||
// Meilisearch配置字段
|
||||
ConfigResponseFieldMeilisearchEnabled = "meilisearch_enabled"
|
||||
ConfigResponseFieldMeilisearchHost = "meilisearch_host"
|
||||
ConfigResponseFieldMeilisearchPort = "meilisearch_port"
|
||||
ConfigResponseFieldMeilisearchMasterKey = "meilisearch_master_key"
|
||||
ConfigResponseFieldMeilisearchIndexName = "meilisearch_index_name"
|
||||
)
|
||||
|
||||
// ConfigDefaultValue 配置默认值常量
|
||||
@@ -120,4 +134,11 @@ const (
|
||||
|
||||
// 三方统计配置默认值
|
||||
ConfigDefaultThirdPartyStatsCode = ""
|
||||
|
||||
// Meilisearch配置默认值
|
||||
ConfigDefaultMeilisearchEnabled = "false"
|
||||
ConfigDefaultMeilisearchHost = "localhost"
|
||||
ConfigDefaultMeilisearchPort = "7700"
|
||||
ConfigDefaultMeilisearchMasterKey = ""
|
||||
ConfigDefaultMeilisearchIndexName = "resources"
|
||||
)
|
||||
|
||||
@@ -34,6 +34,14 @@ type ResourceRepository interface {
|
||||
GetByURL(url string) (*entity.Resource, error)
|
||||
UpdateSaveURL(id uint, saveURL string) error
|
||||
CreateResourceTag(resourceTag *entity.ResourceTag) error
|
||||
FindByIDs(ids []uint) ([]entity.Resource, error)
|
||||
FindUnsyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error)
|
||||
FindSyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error)
|
||||
CountUnsyncedToMeilisearch() (int64, error)
|
||||
CountSyncedToMeilisearch() (int64, error)
|
||||
MarkAsSyncedToMeilisearch(ids []uint) error
|
||||
MarkAllAsUnsyncedToMeilisearch() error
|
||||
FindAllWithPagination(page, limit int) ([]entity.Resource, int64, error)
|
||||
}
|
||||
|
||||
// ResourceRepositoryImpl Resource的Repository实现
|
||||
@@ -461,19 +469,144 @@ func (r *ResourceRepositoryImpl) GetResourcesForTransfer(panID uint, sinceTime t
|
||||
// GetByURL 根据URL获取资源
|
||||
func (r *ResourceRepositoryImpl) GetByURL(url string) (*entity.Resource, error) {
|
||||
var resource entity.Resource
|
||||
err := r.GetDB().Where("url = ?", url).First(&resource).Error
|
||||
err := r.db.Where("url = ?", url).First(&resource).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resource, nil
|
||||
}
|
||||
|
||||
// UpdateSaveURL 更新资源的转存链接
|
||||
// FindByIDs 根据ID列表查找资源
|
||||
func (r *ResourceRepositoryImpl) FindByIDs(ids []uint) ([]entity.Resource, error) {
|
||||
if len(ids) == 0 {
|
||||
return []entity.Resource{}, nil
|
||||
}
|
||||
|
||||
var resources []entity.Resource
|
||||
err := r.db.Where("id IN ?", ids).Preload("Category").Preload("Pan").Preload("Tags").Find(&resources).Error
|
||||
return resources, err
|
||||
}
|
||||
|
||||
// UpdateSaveURL 更新保存URL
|
||||
func (r *ResourceRepositoryImpl) UpdateSaveURL(id uint, saveURL string) error {
|
||||
return r.GetDB().Model(&entity.Resource{}).Where("id = ?", id).Update("save_url", saveURL).Error
|
||||
return r.db.Model(&entity.Resource{}).Where("id = ?", id).Update("save_url", saveURL).Error
|
||||
}
|
||||
|
||||
// CreateResourceTag 创建资源与标签的关联
|
||||
func (r *ResourceRepositoryImpl) CreateResourceTag(resourceTag *entity.ResourceTag) error {
|
||||
return r.GetDB().Create(resourceTag).Error
|
||||
return r.db.Create(resourceTag).Error
|
||||
}
|
||||
|
||||
// FindUnsyncedToMeilisearch 查找未同步到Meilisearch的资源
|
||||
func (r *ResourceRepositoryImpl) FindUnsyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 查询未同步的资源
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", false).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Order("updated_at DESC")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
// CountUnsyncedToMeilisearch 统计未同步到Meilisearch的资源数量
|
||||
func (r *ResourceRepositoryImpl) CountUnsyncedToMeilisearch() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", false).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// MarkAsSyncedToMeilisearch 标记资源为已同步到Meilisearch
|
||||
func (r *ResourceRepositoryImpl) MarkAsSyncedToMeilisearch(ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return r.db.Model(&entity.Resource{}).
|
||||
Where("id IN ?", ids).
|
||||
Updates(map[string]interface{}{
|
||||
"synced_to_meilisearch": true,
|
||||
"synced_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// MarkAllAsUnsyncedToMeilisearch 标记所有资源为未同步到Meilisearch
|
||||
func (r *ResourceRepositoryImpl) MarkAllAsUnsyncedToMeilisearch() error {
|
||||
return r.db.Model(&entity.Resource{}).
|
||||
Where("1 = 1"). // 添加WHERE条件以更新所有记录
|
||||
Updates(map[string]interface{}{
|
||||
"synced_to_meilisearch": false,
|
||||
"synced_at": nil,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// FindSyncedToMeilisearch 查找已同步到Meilisearch的资源
|
||||
func (r *ResourceRepositoryImpl) FindSyncedToMeilisearch(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 查询已同步的资源
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", true).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Order("updated_at DESC")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
// CountSyncedToMeilisearch 统计已同步到Meilisearch的资源数量
|
||||
func (r *ResourceRepositoryImpl) CountSyncedToMeilisearch() (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&entity.Resource{}).
|
||||
Where("synced_to_meilisearch = ?", true).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
// FindAllWithPagination 分页查找所有资源
|
||||
func (r *ResourceRepositoryImpl) FindAllWithPagination(page, limit int) ([]entity.Resource, int64, error) {
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
offset := (page - 1) * limit
|
||||
|
||||
// 查询所有资源
|
||||
db := r.db.Model(&entity.Resource{}).
|
||||
Preload("Category").
|
||||
Preload("Pan").
|
||||
Order("updated_at DESC")
|
||||
|
||||
// 获取总数
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 获取分页数据
|
||||
err := db.Offset(offset).Limit(limit).Find(&resources).Error
|
||||
return resources, total, err
|
||||
}
|
||||
|
||||
@@ -121,10 +121,18 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
{Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyForbiddenWords, Value: entity.ConfigDefaultForbiddenWords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAdKeywords, Value: entity.ConfigDefaultAdKeywords, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyAutoInsertAd, Value: entity.ConfigDefaultAutoInsertAd, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
{Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchEnabled, Value: entity.ConfigDefaultMeilisearchEnabled, Type: entity.ConfigTypeBool},
|
||||
{Key: entity.ConfigKeyMeilisearchHost, Value: entity.ConfigDefaultMeilisearchHost, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
{Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
err = r.UpsertConfigs(defaultConfigs)
|
||||
@@ -149,12 +157,18 @@ func (r *SystemConfigRepositoryImpl) GetOrCreateDefault() ([]entity.SystemConfig
|
||||
entity.ConfigKeyAutoTransferMinSpace: {Key: entity.ConfigKeyAutoTransferMinSpace, Value: entity.ConfigDefaultAutoTransferMinSpace, Type: entity.ConfigTypeInt},
|
||||
entity.ConfigKeyAutoFetchHotDramaEnabled: {Key: entity.ConfigKeyAutoFetchHotDramaEnabled, Value: entity.ConfigDefaultAutoFetchHotDramaEnabled, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyApiToken: {Key: entity.ConfigKeyApiToken, Value: entity.ConfigDefaultApiToken, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyForbiddenWords: {Key: entity.ConfigKeyForbiddenWords, Value: entity.ConfigDefaultForbiddenWords, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyAdKeywords: {Key: entity.ConfigKeyAdKeywords, Value: entity.ConfigDefaultAdKeywords, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyAutoInsertAd: {Key: entity.ConfigKeyAutoInsertAd, Value: entity.ConfigDefaultAutoInsertAd, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyPageSize: {Key: entity.ConfigKeyPageSize, Value: entity.ConfigDefaultPageSize, Type: entity.ConfigTypeInt},
|
||||
entity.ConfigKeyMaintenanceMode: {Key: entity.ConfigKeyMaintenanceMode, Value: entity.ConfigDefaultMaintenanceMode, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyEnableRegister: {Key: entity.ConfigKeyEnableRegister, Value: entity.ConfigDefaultEnableRegister, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyThirdPartyStatsCode: {Key: entity.ConfigKeyThirdPartyStatsCode, Value: entity.ConfigDefaultThirdPartyStatsCode, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchEnabled: {Key: entity.ConfigKeyMeilisearchEnabled, Value: entity.ConfigDefaultMeilisearchEnabled, Type: entity.ConfigTypeBool},
|
||||
entity.ConfigKeyMeilisearchHost: {Key: entity.ConfigKeyMeilisearchHost, Value: entity.ConfigDefaultMeilisearchHost, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchPort: {Key: entity.ConfigKeyMeilisearchPort, Value: entity.ConfigDefaultMeilisearchPort, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchMasterKey: {Key: entity.ConfigKeyMeilisearchMasterKey, Value: entity.ConfigDefaultMeilisearchMasterKey, Type: entity.ConfigTypeString},
|
||||
entity.ConfigKeyMeilisearchIndexName: {Key: entity.ConfigKeyMeilisearchIndexName, Value: entity.ConfigDefaultMeilisearchIndexName, Type: entity.ConfigTypeString},
|
||||
}
|
||||
|
||||
// 检查现有配置中是否有缺失的配置项
|
||||
|
||||
6
go.mod
6
go.mod
@@ -10,11 +10,17 @@ require (
|
||||
github.com/go-resty/resty/v2 v2.16.5
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/meilisearch/meilisearch-go v0.33.1
|
||||
golang.org/x/crypto v0.40.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -1,3 +1,5 @@
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
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=
|
||||
@@ -37,6 +39,8 @@ github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
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.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -81,6 +85,8 @@ github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZ
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/meilisearch/meilisearch-go v0.33.1 h1:IWM8iJU7UyuIoRiTTLONvpbEgMhP/yTrnNfSnxj4wu0=
|
||||
github.com/meilisearch/meilisearch-go v0.33.1/go.mod h1:dY4nxhVc0Ext8Kn7u2YohJCsEjirg80DdcOmfNezUYg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -114,6 +120,8 @@ github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
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=
|
||||
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-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
|
||||
@@ -2,11 +2,18 @@ package handlers
|
||||
|
||||
import (
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/services"
|
||||
)
|
||||
|
||||
var repoManager *repo.RepositoryManager
|
||||
var meilisearchManager *services.MeilisearchManager
|
||||
|
||||
// SetRepositoryManager 设置Repository管理器
|
||||
func SetRepositoryManager(rm *repo.RepositoryManager) {
|
||||
repoManager = rm
|
||||
func SetRepositoryManager(manager *repo.RepositoryManager) {
|
||||
repoManager = manager
|
||||
}
|
||||
|
||||
// SetMeilisearchManager 设置Meilisearch管理器
|
||||
func SetMeilisearchManager(manager *services.MeilisearchManager) {
|
||||
meilisearchManager = manager
|
||||
}
|
||||
|
||||
270
handlers/meilisearch_handler.go
Normal file
270
handlers/meilisearch_handler.go
Normal file
@@ -0,0 +1,270 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/ctwj/urldb/db/converter"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MeilisearchHandler Meilisearch处理器
|
||||
type MeilisearchHandler struct {
|
||||
meilisearchManager *services.MeilisearchManager
|
||||
}
|
||||
|
||||
// NewMeilisearchHandler 创建Meilisearch处理器
|
||||
func NewMeilisearchHandler(meilisearchManager *services.MeilisearchManager) *MeilisearchHandler {
|
||||
return &MeilisearchHandler{
|
||||
meilisearchManager: meilisearchManager,
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnection 测试Meilisearch连接
|
||||
func (h *MeilisearchHandler) TestConnection(c *gin.Context) {
|
||||
var req struct {
|
||||
Host string `json:"host"`
|
||||
Port interface{} `json:"port"` // 支持字符串或数字
|
||||
MasterKey string `json:"masterKey"`
|
||||
IndexName string `json:"indexName"` // 可选字段
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
ErrorResponse(c, "请求参数错误", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必要字段
|
||||
if req.Host == "" {
|
||||
ErrorResponse(c, "主机地址不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 转换port为字符串
|
||||
var portStr string
|
||||
switch v := req.Port.(type) {
|
||||
case string:
|
||||
portStr = v
|
||||
case float64:
|
||||
portStr = strconv.Itoa(int(v))
|
||||
case int:
|
||||
portStr = strconv.Itoa(v)
|
||||
default:
|
||||
portStr = "7700" // 默认端口
|
||||
}
|
||||
|
||||
// 如果没有提供索引名称,使用默认值
|
||||
indexName := req.IndexName
|
||||
if indexName == "" {
|
||||
indexName = "resources"
|
||||
}
|
||||
|
||||
// 创建临时服务进行测试
|
||||
service := services.NewMeilisearchService(req.Host, portStr, req.MasterKey, indexName, true)
|
||||
|
||||
if err := service.HealthCheck(); err != nil {
|
||||
ErrorResponse(c, "连接测试失败: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "连接测试成功"})
|
||||
}
|
||||
|
||||
// GetStatus 获取Meilisearch状态
|
||||
func (h *MeilisearchHandler) GetStatus(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"enabled": false,
|
||||
"healthy": false,
|
||||
"message": "Meilisearch未初始化",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.meilisearchManager.GetStatusWithHealthCheck()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取状态失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, status)
|
||||
}
|
||||
|
||||
// GetUnsyncedCount 获取未同步资源数量
|
||||
func (h *MeilisearchHandler) GetUnsyncedCount(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{"count": 0})
|
||||
return
|
||||
}
|
||||
|
||||
count, err := h.meilisearchManager.GetUnsyncedCount()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取未同步数量失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"count": count})
|
||||
}
|
||||
|
||||
// GetUnsyncedResources 获取未同步的资源
|
||||
func (h *MeilisearchHandler) GetUnsyncedResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
resources, total, err := h.meilisearchManager.GetUnsyncedResources(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取未同步资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetSyncedResources 获取已同步的资源
|
||||
func (h *MeilisearchHandler) GetSyncedResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
resources, total, err := h.meilisearchManager.GetSyncedResources(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取已同步资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAllResources 获取所有资源
|
||||
func (h *MeilisearchHandler) GetAllResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": []interface{}{},
|
||||
"total": 0,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
|
||||
resources, total, err := h.meilisearchManager.GetAllResources(page, pageSize)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "获取所有资源失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"resources": converter.ToResourceResponseList(resources),
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// SyncAllResources 同步所有资源
|
||||
func (h *MeilisearchHandler) SyncAllResources(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("开始同步所有资源到Meilisearch...")
|
||||
|
||||
_, err := h.meilisearchManager.SyncAllResources()
|
||||
if err != nil {
|
||||
ErrorResponse(c, "同步失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "同步已开始,请查看进度",
|
||||
})
|
||||
}
|
||||
|
||||
// GetSyncProgress 获取同步进度
|
||||
func (h *MeilisearchHandler) GetSyncProgress(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
progress := h.meilisearchManager.GetSyncProgress()
|
||||
SuccessResponse(c, progress)
|
||||
}
|
||||
|
||||
// StopSync 停止同步
|
||||
func (h *MeilisearchHandler) StopSync(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.meilisearchManager.StopSync()
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "同步已停止",
|
||||
})
|
||||
}
|
||||
|
||||
// ClearIndex 清空索引
|
||||
func (h *MeilisearchHandler) ClearIndex(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.meilisearchManager.ClearIndex(); err != nil {
|
||||
ErrorResponse(c, "清空索引失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "清空索引成功"})
|
||||
}
|
||||
|
||||
// UpdateIndexSettings 更新索引设置
|
||||
func (h *MeilisearchHandler) UpdateIndexSettings(c *gin.Context) {
|
||||
if h.meilisearchManager == nil {
|
||||
ErrorResponse(c, "Meilisearch未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
service := h.meilisearchManager.GetService()
|
||||
if service == nil {
|
||||
ErrorResponse(c, "Meilisearch服务未初始化", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.UpdateIndexSettings(); err != nil {
|
||||
ErrorResponse(c, "更新索引设置失败: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "索引设置更新成功"})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ctwj/urldb/db/dto"
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -182,6 +183,7 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
keyword := c.Query("keyword")
|
||||
tag := c.Query("tag")
|
||||
category := c.Query("category")
|
||||
panID := c.Query("pan_id")
|
||||
pageStr := c.DefaultQuery("page", "1")
|
||||
pageSizeStr := c.DefaultQuery("page_size", "20")
|
||||
|
||||
@@ -195,29 +197,88 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 构建搜索条件
|
||||
params := map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
|
||||
// 如果启用了Meilisearch,优先使用Meilisearch搜索
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
// 构建过滤器
|
||||
filters := make(map[string]interface{})
|
||||
if category != "" {
|
||||
filters["category"] = category
|
||||
}
|
||||
if tag != "" {
|
||||
filters["tags"] = tag
|
||||
}
|
||||
if panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
// 根据pan_id获取pan_name
|
||||
pan, err := repoManager.PanRepository.FindByID(uint(id))
|
||||
if err == nil && pan != nil {
|
||||
filters["pan_name"] = pan.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Meilisearch搜索
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
docs, docTotal, err := service.Search(keyword, filters, page, pageSize)
|
||||
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
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if keyword != "" {
|
||||
params["search"] = keyword
|
||||
}
|
||||
// 如果Meilisearch未启用或搜索失败,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || err != nil {
|
||||
// 构建搜索条件
|
||||
params := map[string]interface{}{
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}
|
||||
|
||||
if tag != "" {
|
||||
params["tag"] = tag
|
||||
}
|
||||
if keyword != "" {
|
||||
params["search"] = keyword
|
||||
}
|
||||
|
||||
if category != "" {
|
||||
params["category"] = category
|
||||
}
|
||||
if tag != "" {
|
||||
params["tag"] = tag
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
if category != "" {
|
||||
params["category"] = category
|
||||
}
|
||||
if panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
params["pan_id"] = uint(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行数据库搜索
|
||||
resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
if err != nil {
|
||||
ErrorResponse(c, "搜索失败: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤违禁词
|
||||
@@ -242,10 +303,10 @@ func (h *PublicAPIHandler) SearchResources(c *gin.Context) {
|
||||
|
||||
// 构建响应数据
|
||||
responseData := gin.H{
|
||||
"list": resourceResponses,
|
||||
"total": filteredTotal,
|
||||
"page": page,
|
||||
"limit": pageSize,
|
||||
"data": resourceResponses,
|
||||
"total": filteredTotal,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
}
|
||||
|
||||
// 如果存在违禁词过滤,添加提醒字段
|
||||
|
||||
@@ -64,7 +64,52 @@ func GetResources(c *gin.Context) {
|
||||
params["pan_name"] = panName
|
||||
}
|
||||
|
||||
resources, total, err := repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
var resources []entity.Resource
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
// 如果有搜索关键词且启用了Meilisearch,优先使用Meilisearch搜索
|
||||
if search := c.Query("search"); search != "" && meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
// 构建Meilisearch过滤器
|
||||
filters := make(map[string]interface{})
|
||||
if panID := c.Query("pan_id"); panID != "" {
|
||||
if id, err := strconv.ParseUint(panID, 10, 32); err == nil {
|
||||
// 直接使用pan_id进行过滤
|
||||
filters["pan_id"] = id
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Meilisearch搜索
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
docs, docTotal, err := service.Search(search, filters, page, pageSize)
|
||||
if err == nil {
|
||||
// 将Meilisearch文档转换为ResourceResponse(包含高亮信息)
|
||||
var resourceResponses []dto.ResourceResponse
|
||||
for _, doc := range docs {
|
||||
resourceResponse := converter.ToResourceResponseFromMeilisearch(doc)
|
||||
resourceResponses = append(resourceResponses, resourceResponse)
|
||||
}
|
||||
|
||||
// 返回Meilisearch搜索结果(包含高亮信息)
|
||||
SuccessResponse(c, gin.H{
|
||||
"data": resourceResponses,
|
||||
"total": docTotal,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"source": "meilisearch",
|
||||
})
|
||||
return
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果Meilisearch未启用、搜索失败或没有搜索关键词,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || len(resources) == 0 {
|
||||
resources, total, err = repoManager.ResourceRepository.SearchWithFilters(params)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ErrorResponse(c, err.Error(), http.StatusInternalServerError)
|
||||
@@ -164,6 +209,15 @@ func CreateResource(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到Meilisearch
|
||||
if meilisearchManager != nil {
|
||||
go func() {
|
||||
if err := meilisearchManager.SyncResourceToMeilisearch(resource); err != nil {
|
||||
utils.Error("同步资源到Meilisearch失败: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{
|
||||
"message": "资源创建成功",
|
||||
"resource": converter.ToResourceResponse(resource),
|
||||
@@ -240,6 +294,15 @@ func UpdateResource(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到Meilisearch
|
||||
if meilisearchManager != nil {
|
||||
go func() {
|
||||
if err := meilisearchManager.SyncResourceToMeilisearch(resource); err != nil {
|
||||
utils.Error("同步资源到Meilisearch失败: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
SuccessResponse(c, gin.H{"message": "资源更新成功"})
|
||||
}
|
||||
|
||||
@@ -271,12 +334,53 @@ func SearchResources(c *gin.Context) {
|
||||
var total int64
|
||||
var err error
|
||||
|
||||
if query == "" {
|
||||
// 搜索关键词为空时,返回最新记录(分页)
|
||||
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
|
||||
} else {
|
||||
// 有搜索关键词时,执行搜索
|
||||
resources, total, err = repoManager.ResourceRepository.Search(query, nil, page, pageSize)
|
||||
// 如果启用了Meilisearch,优先使用Meilisearch搜索
|
||||
if meilisearchManager != nil && meilisearchManager.IsEnabled() {
|
||||
// 构建过滤器
|
||||
filters := make(map[string]interface{})
|
||||
if categoryID := c.Query("category_id"); categoryID != "" {
|
||||
if id, err := strconv.ParseUint(categoryID, 10, 32); err == nil {
|
||||
filters["category"] = uint(id)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Meilisearch搜索
|
||||
service := meilisearchManager.GetService()
|
||||
if service != nil {
|
||||
docs, docTotal, err := service.Search(query, filters, page, pageSize)
|
||||
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
|
||||
} else {
|
||||
utils.Error("Meilisearch搜索失败,回退到数据库搜索: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果Meilisearch未启用或搜索失败,使用数据库搜索
|
||||
if meilisearchManager == nil || !meilisearchManager.IsEnabled() || err != nil {
|
||||
if query == "" {
|
||||
// 搜索关键词为空时,返回最新记录(分页)
|
||||
resources, total, err = repoManager.ResourceRepository.FindWithRelationsPaginated(page, pageSize)
|
||||
} else {
|
||||
// 有搜索关键词时,执行搜索
|
||||
resources, total, err = repoManager.ResourceRepository.Search(query, nil, page, pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -195,6 +195,17 @@ func UpdateSystemConfig(c *gin.Context) {
|
||||
// 刷新系统配置缓存
|
||||
pan.RefreshSystemConfigCache()
|
||||
|
||||
// 重新加载Meilisearch配置(如果Meilisearch配置有变更)
|
||||
if req.MeilisearchEnabled != nil || req.MeilisearchHost != nil || req.MeilisearchPort != nil || req.MeilisearchMasterKey != nil || req.MeilisearchIndexName != nil {
|
||||
if meilisearchManager != nil {
|
||||
if err := meilisearchManager.ReloadConfig(); err != nil {
|
||||
utils.Error("重新加载Meilisearch配置失败: %v", err)
|
||||
} else {
|
||||
utils.Debug("Meilisearch配置重新加载成功")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据配置更新定时任务状态(错误不影响配置保存)
|
||||
scheduler := scheduler.GetGlobalScheduler(
|
||||
repoManager.HotDramaRepository,
|
||||
|
||||
@@ -239,7 +239,7 @@ func (h *TaskHandler) GetTasks(c *gin.Context) {
|
||||
taskType := c.Query("task_type")
|
||||
status := c.Query("status")
|
||||
|
||||
utils.Info("GetTasks: 获取任务列表 page=%d, pageSize=%d, taskType=%s, status=%s", page, pageSize, taskType, status)
|
||||
utils.Debug("GetTasks: 获取任务列表 page=%d, pageSize=%d, taskType=%s, status=%s", page, pageSize, taskType, status)
|
||||
|
||||
tasks, total, err := h.repoMgr.TaskRepository.GetList(page, pageSize, taskType, status)
|
||||
if err != nil {
|
||||
@@ -248,13 +248,13 @@ func (h *TaskHandler) GetTasks(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
utils.Info("GetTasks: 从数据库获取到 %d 个任务", len(tasks))
|
||||
utils.Debug("GetTasks: 从数据库获取到 %d 个任务", len(tasks))
|
||||
|
||||
// 为每个任务添加运行状态
|
||||
var result []gin.H
|
||||
for _, task := range tasks {
|
||||
isRunning := h.taskManager.IsTaskRunning(task.ID)
|
||||
utils.Info("GetTasks: 任务 %d (%s) 数据库状态: %s, TaskManager运行状态: %v", task.ID, task.Title, task.Status, isRunning)
|
||||
utils.Debug("GetTasks: 任务 %d (%s) 数据库状态: %s, TaskManager运行状态: %v", task.ID, task.Title, task.Status, isRunning)
|
||||
|
||||
result = append(result, gin.H{
|
||||
"id": task.ID,
|
||||
|
||||
26
main.go
26
main.go
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/handlers"
|
||||
"github.com/ctwj/urldb/middleware"
|
||||
"github.com/ctwj/urldb/services"
|
||||
"github.com/ctwj/urldb/task"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
|
||||
@@ -76,6 +77,12 @@ func main() {
|
||||
transferProcessor := task.NewTransferProcessor(repoManager)
|
||||
taskManager.RegisterProcessor(transferProcessor)
|
||||
|
||||
// 初始化Meilisearch管理器
|
||||
meilisearchManager := services.NewMeilisearchManager(repoManager)
|
||||
if err := meilisearchManager.Initialize(); err != nil {
|
||||
utils.Error("初始化Meilisearch管理器失败: %v", err)
|
||||
}
|
||||
|
||||
// 恢复运行中的任务(服务器重启后)
|
||||
if err := taskManager.RecoverRunningTasks(); err != nil {
|
||||
utils.Error("恢复运行中任务失败: %v", err)
|
||||
@@ -98,6 +105,9 @@ func main() {
|
||||
// 将Repository管理器注入到handlers中
|
||||
handlers.SetRepositoryManager(repoManager)
|
||||
|
||||
// 设置Meilisearch管理器到handlers中
|
||||
handlers.SetMeilisearchManager(meilisearchManager)
|
||||
|
||||
// 设置公开API中间件的Repository管理器
|
||||
middleware.SetRepositoryManager(repoManager)
|
||||
|
||||
@@ -110,6 +120,9 @@ func main() {
|
||||
// 创建文件处理器
|
||||
fileHandler := handlers.NewFileHandler(repoManager.FileRepository, repoManager.SystemConfigRepository, repoManager.UserRepository)
|
||||
|
||||
// 创建Meilisearch处理器
|
||||
meilisearchHandler := handlers.NewMeilisearchHandler(meilisearchManager)
|
||||
|
||||
// API路由
|
||||
api := r.Group("/api")
|
||||
{
|
||||
@@ -242,6 +255,19 @@ func main() {
|
||||
api.GET("/version/full", handlers.GetFullVersionInfo)
|
||||
api.GET("/version/check-update", handlers.CheckUpdate)
|
||||
|
||||
// Meilisearch管理路由
|
||||
api.GET("/meilisearch/status", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetStatus)
|
||||
api.GET("/meilisearch/unsynced-count", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetUnsyncedCount)
|
||||
api.GET("/meilisearch/unsynced", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetUnsyncedResources)
|
||||
api.GET("/meilisearch/synced", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetSyncedResources)
|
||||
api.GET("/meilisearch/resources", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetAllResources)
|
||||
api.POST("/meilisearch/sync-all", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.SyncAllResources)
|
||||
api.GET("/meilisearch/sync-progress", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.GetSyncProgress)
|
||||
api.POST("/meilisearch/stop-sync", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.StopSync)
|
||||
api.POST("/meilisearch/clear-index", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.ClearIndex)
|
||||
api.POST("/meilisearch/test-connection", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.TestConnection)
|
||||
api.POST("/meilisearch/update-settings", middleware.AuthMiddleware(), middleware.AdminMiddleware(), meilisearchHandler.UpdateIndexSettings)
|
||||
|
||||
// 文件上传相关路由
|
||||
api.POST("/files/upload", middleware.AuthMiddleware(), fileHandler.UploadFile)
|
||||
api.GET("/files", middleware.AuthMiddleware(), fileHandler.GetFileList)
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
-- 添加文件哈希字段
|
||||
ALTER TABLE files ADD COLUMN file_hash VARCHAR(64) COMMENT '文件哈希值';
|
||||
CREATE UNIQUE INDEX idx_files_hash ON files(file_hash);
|
||||
CREATE UNIQUE INDEX idx_files_hash ON files(file_hash);
|
||||
|
||||
-- 添加同步状态字段
|
||||
ALTER TABLE resources ADD COLUMN synced_to_meilisearch BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE resources ADD COLUMN synced_at TIMESTAMP NULL;
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX idx_resources_synced ON resources(synced_to_meilisearch, synced_at);
|
||||
|
||||
-- 添加注释
|
||||
COMMENT ON COLUMN resources.synced_to_meilisearch IS '是否已同步到Meilisearch';
|
||||
COMMENT ON COLUMN resources.synced_at IS '同步时间';
|
||||
762
services/meilisearch_manager.go
Normal file
762
services/meilisearch_manager.go
Normal file
@@ -0,0 +1,762 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/db/entity"
|
||||
"github.com/ctwj/urldb/db/repo"
|
||||
"github.com/ctwj/urldb/utils"
|
||||
)
|
||||
|
||||
// MeilisearchManager Meilisearch管理器
|
||||
type MeilisearchManager struct {
|
||||
service *MeilisearchService
|
||||
repoMgr *repo.RepositoryManager
|
||||
configRepo repo.SystemConfigRepository
|
||||
mutex sync.RWMutex
|
||||
status MeilisearchStatus
|
||||
stopChan chan struct{}
|
||||
isRunning bool
|
||||
|
||||
// 同步进度控制
|
||||
syncMutex sync.RWMutex
|
||||
syncProgress SyncProgress
|
||||
isSyncing bool
|
||||
syncStopChan chan struct{}
|
||||
}
|
||||
|
||||
// SyncProgress 同步进度
|
||||
type SyncProgress struct {
|
||||
IsRunning bool `json:"is_running"`
|
||||
TotalCount int64 `json:"total_count"`
|
||||
ProcessedCount int64 `json:"processed_count"`
|
||||
SyncedCount int64 `json:"synced_count"`
|
||||
FailedCount int64 `json:"failed_count"`
|
||||
StartTime time.Time `json:"start_time"`
|
||||
EstimatedTime string `json:"estimated_time"`
|
||||
CurrentBatch int `json:"current_batch"`
|
||||
TotalBatches int `json:"total_batches"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
}
|
||||
|
||||
// MeilisearchStatus Meilisearch状态
|
||||
type MeilisearchStatus struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Healthy bool `json:"healthy"`
|
||||
LastCheck time.Time `json:"last_check"`
|
||||
ErrorCount int `json:"error_count"`
|
||||
LastError string `json:"last_error"`
|
||||
DocumentCount int64 `json:"document_count"`
|
||||
}
|
||||
|
||||
// NewMeilisearchManager 创建Meilisearch管理器
|
||||
func NewMeilisearchManager(repoMgr *repo.RepositoryManager) *MeilisearchManager {
|
||||
return &MeilisearchManager{
|
||||
repoMgr: repoMgr,
|
||||
stopChan: make(chan struct{}),
|
||||
syncStopChan: make(chan struct{}),
|
||||
status: MeilisearchStatus{
|
||||
Enabled: false,
|
||||
Healthy: false,
|
||||
LastCheck: time.Now(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize 初始化Meilisearch服务
|
||||
func (m *MeilisearchManager) Initialize() error {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
// 设置configRepo
|
||||
m.configRepo = m.repoMgr.SystemConfigRepository
|
||||
|
||||
// 获取配置
|
||||
enabled, err := m.configRepo.GetConfigBool(entity.ConfigKeyMeilisearchEnabled)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch启用状态失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
utils.Debug("Meilisearch未启用,清理服务状态")
|
||||
m.status.Enabled = false
|
||||
m.service = nil
|
||||
// 停止监控循环
|
||||
if m.stopChan != nil {
|
||||
close(m.stopChan)
|
||||
m.stopChan = make(chan struct{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
host, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchHost)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch主机配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
port, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchPort)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch端口配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
masterKey, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchMasterKey)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch主密钥配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
indexName, err := m.configRepo.GetConfigValue(entity.ConfigKeyMeilisearchIndexName)
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch索引名配置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m.service = NewMeilisearchService(host, port, masterKey, indexName, enabled)
|
||||
m.status.Enabled = enabled
|
||||
|
||||
// 如果启用,创建索引并更新设置
|
||||
if enabled {
|
||||
utils.Debug("Meilisearch已启用,创建索引并更新设置")
|
||||
|
||||
// 创建索引
|
||||
if err := m.service.CreateIndex(); err != nil {
|
||||
utils.Error("创建Meilisearch索引失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
if err := m.service.UpdateIndexSettings(); err != nil {
|
||||
utils.Error("更新Meilisearch索引设置失败: %v", err)
|
||||
}
|
||||
|
||||
// 立即进行一次健康检查
|
||||
go func() {
|
||||
m.checkHealth()
|
||||
// 启动监控
|
||||
go m.monitorLoop()
|
||||
}()
|
||||
} else {
|
||||
utils.Debug("Meilisearch未启用")
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch服务初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsEnabled 检查是否启用
|
||||
func (m *MeilisearchManager) IsEnabled() bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.status.Enabled
|
||||
}
|
||||
|
||||
// ReloadConfig 重新加载配置
|
||||
func (m *MeilisearchManager) ReloadConfig() error {
|
||||
utils.Debug("重新加载Meilisearch配置")
|
||||
return m.Initialize()
|
||||
}
|
||||
|
||||
// GetService 获取Meilisearch服务
|
||||
func (m *MeilisearchManager) GetService() *MeilisearchService {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.service
|
||||
}
|
||||
|
||||
// GetStatus 获取状态
|
||||
func (m *MeilisearchManager) GetStatus() (MeilisearchStatus, error) {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
utils.Debug("获取Meilisearch状态 - 启用状态: %v, 健康状态: %v, 服务实例: %v", m.status.Enabled, m.status.Healthy, m.service != nil)
|
||||
|
||||
if m.service != nil && m.service.IsEnabled() {
|
||||
utils.Debug("Meilisearch服务已初始化且启用,尝试获取索引统计")
|
||||
|
||||
// 获取索引统计
|
||||
stats, err := m.service.GetIndexStats()
|
||||
if err != nil {
|
||||
utils.Error("获取Meilisearch索引统计失败: %v", err)
|
||||
// 即使获取统计失败,也返回当前状态
|
||||
} else {
|
||||
utils.Debug("Meilisearch索引统计: %+v", stats)
|
||||
|
||||
// 更新文档数量
|
||||
if count, ok := stats["numberOfDocuments"].(float64); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
utils.Debug("文档数量 (float64): %d", int64(count))
|
||||
} else if count, ok := stats["numberOfDocuments"].(int64); ok {
|
||||
m.status.DocumentCount = count
|
||||
utils.Debug("文档数量 (int64): %d", count)
|
||||
} else if count, ok := stats["numberOfDocuments"].(int); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
utils.Debug("文档数量 (int): %d", int64(count))
|
||||
} else {
|
||||
utils.Error("无法解析文档数量,类型: %T, 值: %v", stats["numberOfDocuments"], stats["numberOfDocuments"])
|
||||
}
|
||||
|
||||
// 不更新启用状态,保持配置中的状态
|
||||
// 启用状态应该由配置控制,而不是由服务状态控制
|
||||
}
|
||||
} else {
|
||||
utils.Debug("Meilisearch服务未初始化或未启用 - service: %v, enabled: %v", m.service != nil, m.service != nil && m.service.IsEnabled())
|
||||
}
|
||||
|
||||
return m.status, nil
|
||||
}
|
||||
|
||||
// GetStatusWithHealthCheck 获取状态并同时进行健康检查
|
||||
func (m *MeilisearchManager) GetStatusWithHealthCheck() (MeilisearchStatus, error) {
|
||||
// 先进行健康检查
|
||||
m.checkHealth()
|
||||
|
||||
// 然后获取状态
|
||||
return m.GetStatus()
|
||||
}
|
||||
|
||||
// SyncResourceToMeilisearch 同步资源到Meilisearch
|
||||
func (m *MeilisearchManager) SyncResourceToMeilisearch(resource *entity.Resource) error {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return nil
|
||||
}
|
||||
|
||||
doc := m.convertResourceToDocument(resource)
|
||||
err := m.service.BatchAddDocuments([]MeilisearchDocument{doc})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 标记为已同步
|
||||
return m.repoMgr.ResourceRepository.MarkAsSyncedToMeilisearch([]uint{resource.ID})
|
||||
}
|
||||
|
||||
// SyncAllResources 同步所有资源
|
||||
func (m *MeilisearchManager) SyncAllResources() (int, error) {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return 0, fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 检查是否已经在同步中
|
||||
m.syncMutex.Lock()
|
||||
if m.isSyncing {
|
||||
m.syncMutex.Unlock()
|
||||
return 0, fmt.Errorf("同步操作正在进行中")
|
||||
}
|
||||
|
||||
// 初始化同步状态
|
||||
m.isSyncing = true
|
||||
m.syncProgress = SyncProgress{
|
||||
IsRunning: true,
|
||||
TotalCount: 0,
|
||||
ProcessedCount: 0,
|
||||
SyncedCount: 0,
|
||||
FailedCount: 0,
|
||||
StartTime: time.Now(),
|
||||
CurrentBatch: 0,
|
||||
TotalBatches: 0,
|
||||
ErrorMessage: "",
|
||||
}
|
||||
// 重新创建停止通道
|
||||
m.syncStopChan = make(chan struct{})
|
||||
m.syncMutex.Unlock()
|
||||
|
||||
// 在goroutine中执行同步,避免阻塞
|
||||
go func() {
|
||||
defer func() {
|
||||
m.syncMutex.Lock()
|
||||
m.isSyncing = false
|
||||
m.syncProgress.IsRunning = false
|
||||
m.syncMutex.Unlock()
|
||||
}()
|
||||
|
||||
m.syncAllResourcesInternal()
|
||||
}()
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// DebugGetAllDocuments 调试:获取所有文档
|
||||
func (m *MeilisearchManager) DebugGetAllDocuments() error {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
utils.Debug("开始调试:获取Meilisearch中的所有文档")
|
||||
_, err := m.service.GetAllDocuments()
|
||||
if err != nil {
|
||||
utils.Error("调试获取所有文档失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Debug("调试完成:已获取所有文档")
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncAllResourcesInternal 内部同步方法
|
||||
func (m *MeilisearchManager) syncAllResourcesInternal() {
|
||||
// 健康检查
|
||||
if err := m.service.HealthCheck(); err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("Meilisearch不可用: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
if err := m.service.CreateIndex(); err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("创建索引失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("开始同步所有资源到Meilisearch...")
|
||||
|
||||
// 获取总资源数量
|
||||
totalCount, err := m.repoMgr.ResourceRepository.CountUnsyncedToMeilisearch()
|
||||
if err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("获取资源总数失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 分批处理
|
||||
batchSize := 100
|
||||
totalBatches := int((totalCount + int64(batchSize) - 1) / int64(batchSize))
|
||||
|
||||
// 更新总数量和总批次
|
||||
m.syncMutex.Lock()
|
||||
m.syncProgress.TotalCount = totalCount
|
||||
m.syncProgress.TotalBatches = totalBatches
|
||||
m.syncMutex.Unlock()
|
||||
|
||||
offset := 0
|
||||
totalSynced := 0
|
||||
currentBatch := 0
|
||||
|
||||
// 预加载所有分类和平台数据到缓存
|
||||
categoryCache := make(map[uint]string)
|
||||
panCache := make(map[uint]string)
|
||||
|
||||
// 获取所有分类
|
||||
categories, err := m.repoMgr.CategoryRepository.FindAll()
|
||||
if err == nil {
|
||||
for _, category := range categories {
|
||||
categoryCache[category.ID] = category.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有平台
|
||||
pans, err := m.repoMgr.PanRepository.FindAll()
|
||||
if err == nil {
|
||||
for _, pan := range pans {
|
||||
panCache[pan.ID] = pan.Name
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
currentBatch++
|
||||
|
||||
// 获取一批资源(在goroutine中执行,避免阻塞)
|
||||
resourcesChan := make(chan []entity.Resource, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
// 直接查询未同步的资源,不使用分页
|
||||
resources, _, err := m.repoMgr.ResourceRepository.FindUnsyncedToMeilisearch(1, batchSize)
|
||||
if err != nil {
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
resourcesChan <- resources
|
||||
}()
|
||||
|
||||
// 等待数据库查询结果或停止信号(添加超时)
|
||||
select {
|
||||
case resources := <-resourcesChan:
|
||||
if len(resources) == 0 {
|
||||
utils.Info("资源同步完成,总共同步 %d 个资源", totalSynced)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 转换为Meilisearch文档(使用缓存)
|
||||
var docs []MeilisearchDocument
|
||||
for _, resource := range resources {
|
||||
doc := m.convertResourceToDocumentWithCache(&resource, categoryCache, panCache)
|
||||
docs = append(docs, doc)
|
||||
}
|
||||
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 批量添加到Meilisearch(在goroutine中执行,避免阻塞)
|
||||
meilisearchErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
err := m.service.BatchAddDocuments(docs)
|
||||
meilisearchErrChan <- err
|
||||
}()
|
||||
|
||||
// 等待Meilisearch操作结果或停止信号(添加超时)
|
||||
select {
|
||||
case err := <-meilisearchErrChan:
|
||||
if err != nil {
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("批量添加文档失败: %v", err))
|
||||
return
|
||||
}
|
||||
case <-time.After(60 * time.Second): // 60秒超时
|
||||
m.updateSyncProgress("", "", "Meilisearch操作超时")
|
||||
utils.Error("Meilisearch操作超时")
|
||||
return
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否需要停止
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 标记为已同步(在goroutine中执行,避免阻塞)
|
||||
var resourceIDs []uint
|
||||
for _, resource := range resources {
|
||||
resourceIDs = append(resourceIDs, resource.ID)
|
||||
}
|
||||
|
||||
markErrChan := make(chan error, 1)
|
||||
go func() {
|
||||
err := m.repoMgr.ResourceRepository.MarkAsSyncedToMeilisearch(resourceIDs)
|
||||
markErrChan <- err
|
||||
}()
|
||||
|
||||
// 等待标记操作结果或停止信号(添加超时)
|
||||
select {
|
||||
case err := <-markErrChan:
|
||||
if err != nil {
|
||||
utils.Error("标记资源同步状态失败: %v", err)
|
||||
}
|
||||
case <-time.After(30 * time.Second): // 30秒超时
|
||||
utils.Error("标记资源同步状态超时")
|
||||
case <-m.syncStopChan:
|
||||
utils.Debug("同步操作被停止")
|
||||
return
|
||||
}
|
||||
|
||||
totalSynced += len(docs)
|
||||
offset += len(resources)
|
||||
|
||||
// 更新进度
|
||||
m.updateSyncProgress(fmt.Sprintf("%d", totalSynced), fmt.Sprintf("%d", currentBatch), "")
|
||||
|
||||
utils.Debug("已同步 %d 个资源到Meilisearch (批次 %d/%d)", totalSynced, currentBatch, totalBatches)
|
||||
|
||||
// 检查是否已经同步完所有资源
|
||||
if len(resources) == 0 {
|
||||
utils.Info("资源同步完成,总共同步 %d 个资源", totalSynced)
|
||||
return
|
||||
}
|
||||
|
||||
case <-time.After(30 * time.Second): // 30秒超时
|
||||
m.updateSyncProgress("", "", "数据库查询超时")
|
||||
utils.Error("数据库查询超时")
|
||||
return
|
||||
|
||||
case err := <-errChan:
|
||||
m.updateSyncProgress("", "", fmt.Sprintf("获取资源失败: %v", err))
|
||||
return
|
||||
case <-m.syncStopChan:
|
||||
utils.Info("同步操作被停止")
|
||||
return
|
||||
}
|
||||
|
||||
// 避免过于频繁的请求
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
utils.Info("资源同步完成,总共同步 %d 个资源", totalSynced)
|
||||
}
|
||||
|
||||
// updateSyncProgress 更新同步进度
|
||||
func (m *MeilisearchManager) updateSyncProgress(syncedCount, currentBatch, errorMessage string) {
|
||||
m.syncMutex.Lock()
|
||||
defer m.syncMutex.Unlock()
|
||||
|
||||
if syncedCount != "" {
|
||||
if count, err := strconv.ParseInt(syncedCount, 10, 64); err == nil {
|
||||
m.syncProgress.SyncedCount = count
|
||||
}
|
||||
}
|
||||
|
||||
if currentBatch != "" {
|
||||
if batch, err := strconv.Atoi(currentBatch); err == nil {
|
||||
m.syncProgress.CurrentBatch = batch
|
||||
}
|
||||
}
|
||||
|
||||
if errorMessage != "" {
|
||||
m.syncProgress.ErrorMessage = errorMessage
|
||||
m.syncProgress.IsRunning = false
|
||||
}
|
||||
|
||||
// 计算预估时间
|
||||
if m.syncProgress.SyncedCount > 0 {
|
||||
elapsed := time.Since(m.syncProgress.StartTime)
|
||||
rate := float64(m.syncProgress.SyncedCount) / elapsed.Seconds()
|
||||
if rate > 0 {
|
||||
remaining := float64(m.syncProgress.TotalCount-m.syncProgress.SyncedCount) / rate
|
||||
m.syncProgress.EstimatedTime = fmt.Sprintf("%.0f秒", remaining)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUnsyncedCount 获取未同步资源数量
|
||||
func (m *MeilisearchManager) GetUnsyncedCount() (int64, error) {
|
||||
// 直接查询未同步的资源数量
|
||||
return m.repoMgr.ResourceRepository.CountUnsyncedToMeilisearch()
|
||||
}
|
||||
|
||||
// GetUnsyncedResources 获取未同步的资源
|
||||
func (m *MeilisearchManager) GetUnsyncedResources(page, pageSize int) ([]entity.Resource, int64, error) {
|
||||
// 查询未同步到Meilisearch的资源
|
||||
return m.repoMgr.ResourceRepository.FindUnsyncedToMeilisearch(page, pageSize)
|
||||
}
|
||||
|
||||
// GetSyncedResources 获取已同步的资源
|
||||
func (m *MeilisearchManager) GetSyncedResources(page, pageSize int) ([]entity.Resource, int64, error) {
|
||||
// 查询已同步到Meilisearch的资源
|
||||
return m.repoMgr.ResourceRepository.FindSyncedToMeilisearch(page, pageSize)
|
||||
}
|
||||
|
||||
// GetAllResources 获取所有资源
|
||||
func (m *MeilisearchManager) GetAllResources(page, pageSize int) ([]entity.Resource, int64, error) {
|
||||
// 查询所有资源
|
||||
return m.repoMgr.ResourceRepository.FindAllWithPagination(page, pageSize)
|
||||
}
|
||||
|
||||
// GetSyncProgress 获取同步进度
|
||||
func (m *MeilisearchManager) GetSyncProgress() SyncProgress {
|
||||
m.syncMutex.RLock()
|
||||
defer m.syncMutex.RUnlock()
|
||||
return m.syncProgress
|
||||
}
|
||||
|
||||
// StopSync 停止同步
|
||||
func (m *MeilisearchManager) StopSync() {
|
||||
m.syncMutex.Lock()
|
||||
defer m.syncMutex.Unlock()
|
||||
|
||||
if m.isSyncing {
|
||||
// 发送停止信号
|
||||
select {
|
||||
case <-m.syncStopChan:
|
||||
// 通道已经关闭,不需要再次关闭
|
||||
default:
|
||||
close(m.syncStopChan)
|
||||
}
|
||||
|
||||
m.isSyncing = false
|
||||
m.syncProgress.IsRunning = false
|
||||
m.syncProgress.ErrorMessage = "同步已停止"
|
||||
utils.Debug("同步操作已停止")
|
||||
}
|
||||
}
|
||||
|
||||
// ClearIndex 清空索引
|
||||
func (m *MeilisearchManager) ClearIndex() error {
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 清空Meilisearch索引
|
||||
if err := m.service.ClearIndex(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 标记所有资源为未同步
|
||||
return m.repoMgr.ResourceRepository.MarkAllAsUnsyncedToMeilisearch()
|
||||
}
|
||||
|
||||
// convertResourceToDocument 转换资源为搜索文档
|
||||
func (m *MeilisearchManager) convertResourceToDocument(resource *entity.Resource) MeilisearchDocument {
|
||||
// 获取关联数据
|
||||
var categoryName string
|
||||
if resource.CategoryID != nil {
|
||||
category, err := m.repoMgr.CategoryRepository.FindByID(*resource.CategoryID)
|
||||
if err == nil {
|
||||
categoryName = category.Name
|
||||
}
|
||||
}
|
||||
|
||||
var panName string
|
||||
if resource.PanID != nil {
|
||||
pan, err := m.repoMgr.PanRepository.FindByID(*resource.PanID)
|
||||
if err == nil {
|
||||
panName = pan.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签 - 从关联的Tags字段获取
|
||||
var tagNames []string
|
||||
if resource.Tags != nil {
|
||||
for _, tag := range resource.Tags {
|
||||
tagNames = append(tagNames, tag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return MeilisearchDocument{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
Key: resource.Key,
|
||||
Category: categoryName,
|
||||
Tags: tagNames,
|
||||
PanName: panName,
|
||||
PanID: resource.PanID,
|
||||
Author: resource.Author,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// convertResourceToDocumentWithCache 转换资源为搜索文档(使用缓存)
|
||||
func (m *MeilisearchManager) convertResourceToDocumentWithCache(resource *entity.Resource, categoryCache map[uint]string, panCache map[uint]string) MeilisearchDocument {
|
||||
// 从缓存获取关联数据
|
||||
var categoryName string
|
||||
if resource.CategoryID != nil {
|
||||
if name, exists := categoryCache[*resource.CategoryID]; exists {
|
||||
categoryName = name
|
||||
}
|
||||
}
|
||||
|
||||
var panName string
|
||||
if resource.PanID != nil {
|
||||
if name, exists := panCache[*resource.PanID]; exists {
|
||||
panName = name
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签 - 从关联的Tags字段获取
|
||||
var tagNames []string
|
||||
if resource.Tags != nil {
|
||||
for _, tag := range resource.Tags {
|
||||
tagNames = append(tagNames, tag.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return MeilisearchDocument{
|
||||
ID: resource.ID,
|
||||
Title: resource.Title,
|
||||
Description: resource.Description,
|
||||
URL: resource.URL,
|
||||
SaveURL: resource.SaveURL,
|
||||
FileSize: resource.FileSize,
|
||||
Key: resource.Key,
|
||||
Category: categoryName,
|
||||
Tags: tagNames,
|
||||
PanName: panName,
|
||||
PanID: resource.PanID,
|
||||
Author: resource.Author,
|
||||
CreatedAt: resource.CreatedAt,
|
||||
UpdatedAt: resource.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLoop 监控循环
|
||||
func (m *MeilisearchManager) monitorLoop() {
|
||||
if m.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
m.isRunning = true
|
||||
ticker := time.NewTicker(30 * time.Second) // 每30秒检查一次
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.checkHealth()
|
||||
case <-m.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkHealth 检查健康状态
|
||||
func (m *MeilisearchManager) checkHealth() {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
m.status.LastCheck = time.Now()
|
||||
|
||||
utils.Debug("开始健康检查 - 服务实例: %v, 启用状态: %v", m.service != nil, m.service != nil && m.service.IsEnabled())
|
||||
|
||||
if m.service == nil || !m.service.IsEnabled() {
|
||||
utils.Debug("Meilisearch服务未初始化或未启用")
|
||||
m.status.Healthy = false
|
||||
m.status.LastError = "Meilisearch未启用"
|
||||
return
|
||||
}
|
||||
|
||||
utils.Debug("开始检查Meilisearch健康状态")
|
||||
|
||||
if err := m.service.HealthCheck(); err != nil {
|
||||
m.status.Healthy = false
|
||||
m.status.ErrorCount++
|
||||
m.status.LastError = err.Error()
|
||||
utils.Error("Meilisearch健康检查失败: %v", err)
|
||||
} else {
|
||||
m.status.Healthy = true
|
||||
m.status.ErrorCount = 0
|
||||
m.status.LastError = ""
|
||||
utils.Debug("Meilisearch健康检查成功")
|
||||
|
||||
// 健康检查通过后,更新文档数量
|
||||
if stats, err := m.service.GetIndexStats(); err == nil {
|
||||
if count, ok := stats["numberOfDocuments"].(float64); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
} else if count, ok := stats["numberOfDocuments"].(int64); ok {
|
||||
m.status.DocumentCount = count
|
||||
} else if count, ok := stats["numberOfDocuments"].(int); ok {
|
||||
m.status.DocumentCount = int64(count)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止监控
|
||||
func (m *MeilisearchManager) Stop() {
|
||||
if !m.isRunning {
|
||||
return
|
||||
}
|
||||
|
||||
close(m.stopChan)
|
||||
m.isRunning = false
|
||||
utils.Debug("Meilisearch监控服务已停止")
|
||||
}
|
||||
553
services/meilisearch_service.go
Normal file
553
services/meilisearch_service.go
Normal file
@@ -0,0 +1,553 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/ctwj/urldb/utils"
|
||||
"github.com/meilisearch/meilisearch-go"
|
||||
)
|
||||
|
||||
// MeilisearchDocument 搜索文档结构
|
||||
type MeilisearchDocument struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
URL string `json:"url"`
|
||||
SaveURL string `json:"save_url"`
|
||||
FileSize string `json:"file_size"`
|
||||
Key string `json:"key"`
|
||||
Category string `json:"category"`
|
||||
Tags []string `json:"tags"`
|
||||
PanName string `json:"pan_name"`
|
||||
PanID *uint `json:"pan_id"`
|
||||
Author string `json:"author"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// 高亮字段
|
||||
TitleHighlight string `json:"_title_highlight,omitempty"`
|
||||
DescriptionHighlight string `json:"_description_highlight,omitempty"`
|
||||
CategoryHighlight string `json:"_category_highlight,omitempty"`
|
||||
TagsHighlight []string `json:"_tags_highlight,omitempty"`
|
||||
}
|
||||
|
||||
// MeilisearchService Meilisearch服务
|
||||
type MeilisearchService struct {
|
||||
client meilisearch.ServiceManager
|
||||
index meilisearch.IndexManager
|
||||
indexName string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewMeilisearchService 创建Meilisearch服务
|
||||
func NewMeilisearchService(host, port, masterKey, indexName string, enabled bool) *MeilisearchService {
|
||||
if !enabled {
|
||||
return &MeilisearchService{
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
// 构建服务器URL
|
||||
serverURL := fmt.Sprintf("http://%s:%s", host, port)
|
||||
|
||||
// 创建客户端
|
||||
var client meilisearch.ServiceManager
|
||||
|
||||
if masterKey != "" {
|
||||
client = meilisearch.New(serverURL, meilisearch.WithAPIKey(masterKey))
|
||||
} else {
|
||||
client = meilisearch.New(serverURL)
|
||||
}
|
||||
|
||||
// 获取索引
|
||||
index := client.Index(indexName)
|
||||
|
||||
return &MeilisearchService{
|
||||
client: client,
|
||||
index: index,
|
||||
indexName: indexName,
|
||||
enabled: enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// IsEnabled 检查是否启用
|
||||
func (m *MeilisearchService) IsEnabled() bool {
|
||||
return m.enabled
|
||||
}
|
||||
|
||||
// HealthCheck 健康检查
|
||||
func (m *MeilisearchService) HealthCheck() error {
|
||||
if !m.enabled {
|
||||
utils.Debug("Meilisearch未启用,跳过健康检查")
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
utils.Debug("开始Meilisearch健康检查")
|
||||
|
||||
// 使用官方SDK的健康检查
|
||||
_, err := m.client.Health()
|
||||
if err != nil {
|
||||
utils.Error("Meilisearch健康检查失败: %v", err)
|
||||
return fmt.Errorf("Meilisearch健康检查失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch健康检查成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateIndex 创建索引
|
||||
func (m *MeilisearchService) CreateIndex() error {
|
||||
if !m.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建索引配置
|
||||
indexConfig := &meilisearch.IndexConfig{
|
||||
Uid: m.indexName,
|
||||
PrimaryKey: "id",
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
_, err := m.client.CreateIndex(indexConfig)
|
||||
if err != nil {
|
||||
// 如果索引已存在,返回成功
|
||||
utils.Debug("Meilisearch索引创建失败或已存在: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引创建成功: %s", m.indexName)
|
||||
|
||||
// 配置索引设置
|
||||
settings := &meilisearch.Settings{
|
||||
// 配置可过滤的属性
|
||||
FilterableAttributes: []string{
|
||||
"pan_id",
|
||||
"pan_name",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可搜索的属性
|
||||
SearchableAttributes: []string{
|
||||
"title",
|
||||
"description",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可排序的属性
|
||||
SortableAttributes: []string{
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"id",
|
||||
},
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
_, err = m.index.UpdateSettings(settings)
|
||||
if err != nil {
|
||||
utils.Error("更新Meilisearch索引设置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引设置更新成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateIndexSettings 更新索引设置
|
||||
func (m *MeilisearchService) UpdateIndexSettings() error {
|
||||
if !m.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 配置索引设置
|
||||
settings := &meilisearch.Settings{
|
||||
// 配置可过滤的属性
|
||||
FilterableAttributes: []string{
|
||||
"pan_id",
|
||||
"pan_name",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可搜索的属性
|
||||
SearchableAttributes: []string{
|
||||
"title",
|
||||
"description",
|
||||
"category",
|
||||
"tags",
|
||||
},
|
||||
// 配置可排序的属性
|
||||
SortableAttributes: []string{
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"id",
|
||||
},
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
_, err := m.index.UpdateSettings(settings)
|
||||
if err != nil {
|
||||
utils.Error("更新Meilisearch索引设置失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引设置更新成功")
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchAddDocuments 批量添加文档
|
||||
func (m *MeilisearchService) BatchAddDocuments(docs []MeilisearchDocument) error {
|
||||
if !m.enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(docs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 转换为interface{}切片
|
||||
var documents []interface{}
|
||||
for _, doc := range docs {
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
|
||||
// 批量添加文档
|
||||
_, err := m.index.AddDocuments(documents, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("批量添加文档失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("批量添加 %d 个文档到Meilisearch成功", len(docs))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search 搜索文档
|
||||
func (m *MeilisearchService) Search(query string, filters map[string]interface{}, page, pageSize int) ([]MeilisearchDocument, int64, error) {
|
||||
|
||||
if !m.enabled {
|
||||
return nil, 0, fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 构建搜索请求
|
||||
searchRequest := &meilisearch.SearchRequest{
|
||||
Query: query,
|
||||
Offset: int64((page - 1) * pageSize),
|
||||
Limit: int64(pageSize),
|
||||
// 启用高亮功能
|
||||
AttributesToHighlight: []string{"title", "description", "category", "tags"},
|
||||
HighlightPreTag: "<mark>",
|
||||
HighlightPostTag: "</mark>",
|
||||
}
|
||||
|
||||
// 添加过滤器
|
||||
if len(filters) > 0 {
|
||||
var filterStrings []string
|
||||
for key, value := range filters {
|
||||
switch key {
|
||||
case "pan_id":
|
||||
// 直接使用pan_id进行过滤
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("pan_id = %v", value))
|
||||
case "pan_name":
|
||||
// 使用pan_name进行过滤
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("pan_name = %q", value))
|
||||
case "category":
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("category = %q", value))
|
||||
case "tags":
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("tags = %q", value))
|
||||
default:
|
||||
filterStrings = append(filterStrings, fmt.Sprintf("%s = %q", key, value))
|
||||
}
|
||||
}
|
||||
if len(filterStrings) > 0 {
|
||||
searchRequest.Filter = filterStrings
|
||||
}
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
result, err := m.index.Search(query, searchRequest)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("搜索失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
var documents []MeilisearchDocument
|
||||
|
||||
// 如果没有任何结果,直接返回
|
||||
if len(result.Hits) == 0 {
|
||||
utils.Debug("没有搜索结果")
|
||||
return documents, result.EstimatedTotalHits, nil
|
||||
}
|
||||
|
||||
for _, hit := range result.Hits {
|
||||
// 将hit转换为MeilisearchDocument
|
||||
doc := MeilisearchDocument{}
|
||||
|
||||
// 解析JSON数据 - 使用反射
|
||||
hitValue := reflect.ValueOf(hit)
|
||||
|
||||
if hitValue.Kind() == reflect.Map {
|
||||
for _, key := range hitValue.MapKeys() {
|
||||
keyStr := key.String()
|
||||
value := hitValue.MapIndex(key).Interface()
|
||||
|
||||
// 处理_formatted字段(包含所有高亮内容)
|
||||
if keyStr == "_formatted" {
|
||||
if rawValue, ok := value.(json.RawMessage); ok {
|
||||
// 解析_formatted字段中的高亮内容
|
||||
var formattedData map[string]interface{}
|
||||
if err := json.Unmarshal(rawValue, &formattedData); err == nil {
|
||||
// 提取高亮字段
|
||||
if titleHighlight, ok := formattedData["title"].(string); ok {
|
||||
doc.TitleHighlight = titleHighlight
|
||||
}
|
||||
if descHighlight, ok := formattedData["description"].(string); ok {
|
||||
doc.DescriptionHighlight = descHighlight
|
||||
}
|
||||
if categoryHighlight, ok := formattedData["category"].(string); ok {
|
||||
doc.CategoryHighlight = categoryHighlight
|
||||
}
|
||||
if tagsHighlight, ok := formattedData["tags"].([]interface{}); ok {
|
||||
var tags []string
|
||||
for _, tag := range tagsHighlight {
|
||||
if tagStr, ok := tag.(string); ok {
|
||||
tags = append(tags, tagStr)
|
||||
}
|
||||
}
|
||||
doc.TagsHighlight = tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch keyStr {
|
||||
case "id":
|
||||
if rawID, ok := value.(json.RawMessage); ok {
|
||||
var id float64
|
||||
if err := json.Unmarshal(rawID, &id); err == nil {
|
||||
doc.ID = uint(id)
|
||||
}
|
||||
}
|
||||
case "title":
|
||||
if rawTitle, ok := value.(json.RawMessage); ok {
|
||||
var title string
|
||||
if err := json.Unmarshal(rawTitle, &title); err == nil {
|
||||
doc.Title = title
|
||||
}
|
||||
}
|
||||
case "description":
|
||||
if rawDesc, ok := value.(json.RawMessage); ok {
|
||||
var description string
|
||||
if err := json.Unmarshal(rawDesc, &description); err == nil {
|
||||
doc.Description = description
|
||||
}
|
||||
}
|
||||
case "url":
|
||||
if rawURL, ok := value.(json.RawMessage); ok {
|
||||
var url string
|
||||
if err := json.Unmarshal(rawURL, &url); err == nil {
|
||||
doc.URL = url
|
||||
}
|
||||
}
|
||||
case "save_url":
|
||||
if rawSaveURL, ok := value.(json.RawMessage); ok {
|
||||
var saveURL string
|
||||
if err := json.Unmarshal(rawSaveURL, &saveURL); err == nil {
|
||||
doc.SaveURL = saveURL
|
||||
}
|
||||
}
|
||||
case "file_size":
|
||||
if rawFileSize, ok := value.(json.RawMessage); ok {
|
||||
var fileSize string
|
||||
if err := json.Unmarshal(rawFileSize, &fileSize); err == nil {
|
||||
doc.FileSize = fileSize
|
||||
}
|
||||
}
|
||||
case "key":
|
||||
if rawKey, ok := value.(json.RawMessage); ok {
|
||||
var key string
|
||||
if err := json.Unmarshal(rawKey, &key); err == nil {
|
||||
doc.Key = key
|
||||
}
|
||||
}
|
||||
case "category":
|
||||
if rawCategory, ok := value.(json.RawMessage); ok {
|
||||
var category string
|
||||
if err := json.Unmarshal(rawCategory, &category); err == nil {
|
||||
doc.Category = category
|
||||
}
|
||||
}
|
||||
case "tags":
|
||||
if rawTags, ok := value.(json.RawMessage); ok {
|
||||
var tags []string
|
||||
if err := json.Unmarshal(rawTags, &tags); err == nil {
|
||||
doc.Tags = tags
|
||||
}
|
||||
}
|
||||
case "pan_name":
|
||||
if rawPanName, ok := value.(json.RawMessage); ok {
|
||||
var panName string
|
||||
if err := json.Unmarshal(rawPanName, &panName); err == nil {
|
||||
doc.PanName = panName
|
||||
}
|
||||
}
|
||||
case "pan_id":
|
||||
if rawPanID, ok := value.(json.RawMessage); ok {
|
||||
var panID float64
|
||||
if err := json.Unmarshal(rawPanID, &panID); err == nil {
|
||||
panIDUint := uint(panID)
|
||||
doc.PanID = &panIDUint
|
||||
}
|
||||
}
|
||||
case "author":
|
||||
if rawAuthor, ok := value.(json.RawMessage); ok {
|
||||
var author string
|
||||
if err := json.Unmarshal(rawAuthor, &author); err == nil {
|
||||
doc.Author = author
|
||||
}
|
||||
}
|
||||
case "created_at":
|
||||
if rawCreatedAt, ok := value.(json.RawMessage); ok {
|
||||
var createdAt string
|
||||
if err := json.Unmarshal(rawCreatedAt, &createdAt); err == nil {
|
||||
// 尝试多种时间格式
|
||||
var t time.Time
|
||||
var parseErr error
|
||||
formats := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05Z",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05.000Z",
|
||||
}
|
||||
for _, format := range formats {
|
||||
if t, parseErr = time.Parse(format, createdAt); parseErr == nil {
|
||||
doc.CreatedAt = t
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "updated_at":
|
||||
if rawUpdatedAt, ok := value.(json.RawMessage); ok {
|
||||
var updatedAt string
|
||||
if err := json.Unmarshal(rawUpdatedAt, &updatedAt); err == nil {
|
||||
// 尝试多种时间格式
|
||||
var t time.Time
|
||||
var parseErr error
|
||||
formats := []string{
|
||||
time.RFC3339,
|
||||
"2006-01-02T15:04:05Z",
|
||||
"2006-01-02 15:04:05",
|
||||
"2006-01-02T15:04:05.000Z",
|
||||
}
|
||||
for _, format := range formats {
|
||||
if t, parseErr = time.Parse(format, updatedAt); parseErr == nil {
|
||||
doc.UpdatedAt = t
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 高亮字段处理 - 已移除,现在使用_formatted字段
|
||||
}
|
||||
}
|
||||
} else {
|
||||
utils.Error("hit不是Map类型,无法解析")
|
||||
}
|
||||
|
||||
documents = append(documents, doc)
|
||||
}
|
||||
|
||||
return documents, result.EstimatedTotalHits, nil
|
||||
}
|
||||
|
||||
// GetAllDocuments 获取所有文档(用于调试)
|
||||
func (m *MeilisearchService) GetAllDocuments() ([]MeilisearchDocument, error) {
|
||||
if !m.enabled {
|
||||
return nil, fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 构建搜索请求,获取所有文档
|
||||
searchRequest := &meilisearch.SearchRequest{
|
||||
Query: "",
|
||||
Offset: 0,
|
||||
Limit: 1000, // 获取前1000个文档
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
result, err := m.index.Search("", searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取所有文档失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("获取所有文档,总数: %d", result.EstimatedTotalHits)
|
||||
utils.Debug("获取到的文档数量: %d", len(result.Hits))
|
||||
|
||||
// 解析结果
|
||||
var documents []MeilisearchDocument
|
||||
utils.Debug("获取到 %d 个文档", len(result.Hits))
|
||||
|
||||
// 只显示前3个文档的字段信息
|
||||
for i, hit := range result.Hits {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
utils.Debug("文档%d的字段:", i+1)
|
||||
hitValue := reflect.ValueOf(hit)
|
||||
if hitValue.Kind() == reflect.Map {
|
||||
for _, key := range hitValue.MapKeys() {
|
||||
keyStr := key.String()
|
||||
value := hitValue.MapIndex(key).Interface()
|
||||
if rawValue, ok := value.(json.RawMessage); ok {
|
||||
utils.Debug(" %s: %s", keyStr, string(rawValue))
|
||||
} else {
|
||||
utils.Debug(" %s: %v", keyStr, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return documents, nil
|
||||
}
|
||||
|
||||
// GetIndexStats 获取索引统计信息
|
||||
func (m *MeilisearchService) GetIndexStats() (map[string]interface{}, error) {
|
||||
if !m.enabled {
|
||||
return map[string]interface{}{
|
||||
"enabled": false,
|
||||
"message": "Meilisearch未启用",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 获取索引统计
|
||||
stats, err := m.index.GetStats()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取索引统计失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch统计 - 文档数: %d, 索引中: %v", stats.NumberOfDocuments, stats.IsIndexing)
|
||||
|
||||
// 转换为map
|
||||
result := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"numberOfDocuments": stats.NumberOfDocuments,
|
||||
"isIndexing": stats.IsIndexing,
|
||||
"fieldDistribution": stats.FieldDistribution,
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ClearIndex 清空索引
|
||||
func (m *MeilisearchService) ClearIndex() error {
|
||||
if !m.enabled {
|
||||
return fmt.Errorf("Meilisearch未启用")
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
_, err := m.index.DeleteAllDocuments()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清空索引失败: %v", err)
|
||||
}
|
||||
|
||||
utils.Debug("Meilisearch索引已清空")
|
||||
return nil
|
||||
}
|
||||
@@ -32,4 +32,14 @@
|
||||
.resource-card {
|
||||
@apply bg-white rounded-lg shadow-md border border-gray-200 p-4 hover:shadow-lg transition-shadow duration-200;
|
||||
}
|
||||
|
||||
/* 搜索高亮样式 */
|
||||
mark {
|
||||
@apply bg-yellow-200 text-yellow-900 px-1 py-0.5 rounded font-medium;
|
||||
}
|
||||
|
||||
/* 暗色模式下的高亮样式 */
|
||||
.dark mark {
|
||||
@apply bg-yellow-600 text-yellow-100;
|
||||
}
|
||||
}
|
||||
1
web/components.d.ts
vendored
1
web/components.d.ts
vendored
@@ -39,6 +39,7 @@ declare module 'vue' {
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTable: typeof import('naive-ui')['NTable']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
</div>
|
||||
|
||||
<!-- 自动处理状态 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<NuxtLink to="/admin/feature-config" class="flex items-center space-x-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 px-2 py-1 rounded transition-colors">
|
||||
<div :class="autoProcessEnabled ? 'w-2 h-2 bg-green-500 rounded-full' : 'w-2 h-2 bg-gray-400 rounded-full'"></div>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">
|
||||
自动处理{{ autoProcessEnabled ? '已开启' : '已关闭' }}
|
||||
</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<!-- 自动转存状态 -->
|
||||
<div class="flex items-center space-x-2">
|
||||
|
||||
@@ -225,4 +225,34 @@ function log(...args: any[]) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(...args)
|
||||
}
|
||||
}
|
||||
|
||||
// Meilisearch管理API
|
||||
export const useMeilisearchApi = () => {
|
||||
const getStatus = () => useApiFetch('/meilisearch/status').then(parseApiResponse)
|
||||
const getUnsyncedCount = () => useApiFetch('/meilisearch/unsynced-count').then(parseApiResponse)
|
||||
const getUnsyncedResources = (params?: any) => useApiFetch('/meilisearch/unsynced', { params }).then(parseApiResponse)
|
||||
const getSyncedResources = (params?: any) => useApiFetch('/meilisearch/synced', { params }).then(parseApiResponse)
|
||||
const getAllResources = (params?: any) => useApiFetch('/meilisearch/resources', { params }).then(parseApiResponse)
|
||||
const testConnection = (data: any) => useApiFetch('/meilisearch/test-connection', { method: 'POST', body: data }).then(parseApiResponse)
|
||||
const syncAllResources = () => useApiFetch('/meilisearch/sync-all', { method: 'POST' }).then(parseApiResponse)
|
||||
const stopSync = () => useApiFetch('/meilisearch/stop-sync', { method: 'POST' }).then(parseApiResponse)
|
||||
const clearIndex = () => useApiFetch('/meilisearch/clear-index', { method: 'POST' }).then(parseApiResponse)
|
||||
const updateIndexSettings = () => useApiFetch('/meilisearch/update-settings', { method: 'POST' }).then(parseApiResponse)
|
||||
const getSyncProgress = () => useApiFetch('/meilisearch/sync-progress').then(parseApiResponse)
|
||||
const debugGetAllDocuments = () => useApiFetch('/meilisearch/debug/documents').then(parseApiResponse)
|
||||
return {
|
||||
getStatus,
|
||||
getUnsyncedCount,
|
||||
getUnsyncedResources,
|
||||
getSyncedResources,
|
||||
getAllResources,
|
||||
testConnection,
|
||||
syncAllResources,
|
||||
stopSync,
|
||||
clearIndex,
|
||||
updateIndexSettings,
|
||||
getSyncProgress,
|
||||
debugGetAllDocuments
|
||||
}
|
||||
}
|
||||
@@ -150,6 +150,7 @@ export const adminNewNavigationItems = [
|
||||
active: (route: any) => route.path.startsWith('/admin/site-config'),
|
||||
group: 'system'
|
||||
},
|
||||
|
||||
{
|
||||
key: 'version',
|
||||
label: '版本信息',
|
||||
|
||||
@@ -33,28 +33,137 @@
|
||||
label-width="auto"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<div class="space-y-6">
|
||||
<!-- 自动处理 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">待处理资源自动处理</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">开启后,系统将自动处理待处理的资源,无需手动操作</span>
|
||||
<div class="space-y-8">
|
||||
<!-- 自动处理配置组 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<div class="w-1 h-6 bg-blue-500 rounded-full"></div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">自动处理配置</h3>
|
||||
</div>
|
||||
|
||||
<!-- 自动处理 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">待处理资源自动处理</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">开启后,系统将自动处理待处理的资源,无需手动操作</span>
|
||||
</div>
|
||||
<n-switch v-model:value="configForm.auto_process_enabled" />
|
||||
</div>
|
||||
|
||||
<!-- 自动处理间隔 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">自动处理间隔 (分钟)</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">建议设置 5-60 分钟,避免过于频繁的处理</span>
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="configForm.auto_process_interval"
|
||||
type="text"
|
||||
placeholder="30"
|
||||
:disabled="!configForm.auto_process_enabled"
|
||||
/>
|
||||
</div>
|
||||
<n-switch v-model:value="configForm.auto_process_enabled" />
|
||||
</div>
|
||||
|
||||
<!-- 自动处理间隔 -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">自动处理间隔 (分钟)</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">建议设置 5-60 分钟,避免过于频繁的处理</span>
|
||||
<!-- Meilisearch搜索优化配置组 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<div class="w-1 h-6 bg-green-500 rounded-full"></div>
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">搜索优化配置</h3>
|
||||
</div>
|
||||
|
||||
<!-- 启用Meilisearch -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<label class="text-base font-semibold text-gray-800 dark:text-gray-200">启用Meilisearch搜索优化</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">开启后,系统将使用Meilisearch提供更快的搜索体验</span>
|
||||
</div>
|
||||
<n-switch v-model:value="configForm.meilisearch_enabled" />
|
||||
</div>
|
||||
|
||||
<!-- Meilisearch服务器配置 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" :class="{ 'opacity-50': !configForm.meilisearch_enabled }">
|
||||
<!-- 服务器地址 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">服务器地址</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_host"
|
||||
placeholder="localhost"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 端口 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">端口</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_port"
|
||||
placeholder="7700"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 主密钥 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">主密钥 (可选)</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_master_key"
|
||||
placeholder="留空表示无认证"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 索引名称 -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">索引名称</label>
|
||||
<n-input
|
||||
v-model:value="configForm.meilisearch_index_name"
|
||||
placeholder="resources"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮组 -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<n-button
|
||||
type="info"
|
||||
size="small"
|
||||
:disabled="!configForm.meilisearch_enabled"
|
||||
@click="testMeilisearchConnection"
|
||||
:loading="testingConnection"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-plug"></i>
|
||||
</template>
|
||||
测试连接
|
||||
</n-button>
|
||||
|
||||
<n-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="navigateTo('/admin/meilisearch-management')"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-cogs"></i>
|
||||
</template>
|
||||
搜索优化管理
|
||||
</n-button>
|
||||
|
||||
<!-- 健康状态和未同步数量显示 -->
|
||||
<div v-if="meilisearchStatus" class="flex items-center space-x-4 ml-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-2 h-2 rounded-full" :class="meilisearchStatus.healthy ? 'bg-green-500' : 'bg-red-500'"></div>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">健康状态: {{ meilisearchStatus.healthy ? '正常' : '异常' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<i class="fas fa-sync-alt text-purple-500"></i>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">未同步: {{ unsyncedCount || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="configForm.auto_process_interval"
|
||||
type="text"
|
||||
placeholder="30"
|
||||
:disabled="!configForm.auto_process_enabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-form>
|
||||
@@ -184,6 +293,11 @@ interface FeatureConfigForm {
|
||||
ad_keywords: string
|
||||
auto_insert_ad: string
|
||||
hot_drama_auto_fetch: boolean
|
||||
meilisearch_enabled: boolean
|
||||
meilisearch_host: string
|
||||
meilisearch_port: string
|
||||
meilisearch_master_key: string
|
||||
meilisearch_index_name: string
|
||||
}
|
||||
|
||||
// 使用配置改动检测
|
||||
@@ -204,13 +318,23 @@ const {
|
||||
auto_transfer_min_space: 'auto_transfer_min_space',
|
||||
ad_keywords: 'ad_keywords',
|
||||
auto_insert_ad: 'auto_insert_ad',
|
||||
hot_drama_auto_fetch: 'auto_fetch_hot_drama_enabled'
|
||||
hot_drama_auto_fetch: 'auto_fetch_hot_drama_enabled',
|
||||
meilisearch_enabled: 'meilisearch_enabled',
|
||||
meilisearch_host: 'meilisearch_host',
|
||||
meilisearch_port: 'meilisearch_port',
|
||||
meilisearch_master_key: 'meilisearch_master_key',
|
||||
meilisearch_index_name: 'meilisearch_index_name'
|
||||
}
|
||||
})
|
||||
|
||||
const notification = useNotification()
|
||||
const saving = ref(false)
|
||||
const activeTab = ref('resource')
|
||||
const testingConnection = ref(false)
|
||||
|
||||
// Meilisearch状态
|
||||
const meilisearchStatus = ref<any>(null)
|
||||
const unsyncedCount = ref(0)
|
||||
|
||||
// 配置表单数据
|
||||
const configForm = ref<FeatureConfigForm>({
|
||||
@@ -220,7 +344,12 @@ const configForm = ref<FeatureConfigForm>({
|
||||
auto_transfer_min_space: '500',
|
||||
ad_keywords: '',
|
||||
auto_insert_ad: '',
|
||||
hot_drama_auto_fetch: false
|
||||
hot_drama_auto_fetch: false,
|
||||
meilisearch_enabled: false,
|
||||
meilisearch_host: '',
|
||||
meilisearch_port: '',
|
||||
meilisearch_master_key: '',
|
||||
meilisearch_index_name: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
@@ -241,7 +370,12 @@ const fetchConfig = async () => {
|
||||
auto_transfer_min_space: String(response.auto_transfer_min_space || 500),
|
||||
ad_keywords: response.ad_keywords || '',
|
||||
auto_insert_ad: response.auto_insert_ad || '',
|
||||
hot_drama_auto_fetch: response.auto_fetch_hot_drama_enabled || false
|
||||
hot_drama_auto_fetch: response.auto_fetch_hot_drama_enabled || false,
|
||||
meilisearch_enabled: response.meilisearch_enabled || false,
|
||||
meilisearch_host: response.meilisearch_host || '',
|
||||
meilisearch_port: String(response.meilisearch_port || 7700),
|
||||
meilisearch_master_key: response.meilisearch_master_key || '',
|
||||
meilisearch_index_name: response.meilisearch_index_name || 'resources'
|
||||
}
|
||||
|
||||
configForm.value = { ...configData }
|
||||
@@ -269,7 +403,12 @@ const saveConfig = async () => {
|
||||
auto_transfer_min_space: configForm.value.auto_transfer_min_space,
|
||||
ad_keywords: configForm.value.ad_keywords,
|
||||
auto_insert_ad: configForm.value.auto_insert_ad,
|
||||
hot_drama_auto_fetch: configForm.value.hot_drama_auto_fetch
|
||||
hot_drama_auto_fetch: configForm.value.hot_drama_auto_fetch,
|
||||
meilisearch_enabled: configForm.value.meilisearch_enabled,
|
||||
meilisearch_host: configForm.value.meilisearch_host,
|
||||
meilisearch_port: configForm.value.meilisearch_port,
|
||||
meilisearch_master_key: configForm.value.meilisearch_master_key,
|
||||
meilisearch_index_name: configForm.value.meilisearch_index_name
|
||||
})
|
||||
|
||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||
@@ -327,9 +466,70 @@ const saveConfig = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试Meilisearch连接
|
||||
const testMeilisearchConnection = async () => {
|
||||
testingConnection.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.testConnection({
|
||||
host: configForm.value.meilisearch_host,
|
||||
port: parseInt(configForm.value.meilisearch_port, 10),
|
||||
masterKey: configForm.value.meilisearch_master_key,
|
||||
indexName: configForm.value.meilisearch_index_name || 'resources'
|
||||
})
|
||||
notification.success({
|
||||
content: 'Meilisearch连接测试成功!',
|
||||
duration: 3000
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('Meilisearch连接测试失败:', error)
|
||||
notification.error({
|
||||
content: `Meilisearch连接测试失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
testingConnection.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取Meilisearch状态
|
||||
const fetchMeilisearchStatus = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const status = await meilisearchApi.getStatus()
|
||||
meilisearchStatus.value = status
|
||||
} catch (error: any) {
|
||||
console.error('获取Meilisearch状态失败:', error)
|
||||
notification.error({
|
||||
content: `获取Meilisearch状态失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未同步文档数量
|
||||
const fetchUnsyncedCount = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const response = await meilisearchApi.getUnsyncedCount() as any
|
||||
unsyncedCount.value = response?.count || 0
|
||||
} catch (error: any) {
|
||||
console.error('获取未同步文档数量失败:', error)
|
||||
notification.error({
|
||||
content: `获取未同步文档数量失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取配置
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
fetchMeilisearchStatus()
|
||||
fetchUnsyncedCount()
|
||||
})
|
||||
|
||||
|
||||
|
||||
764
web/pages/admin/meilisearch-management.vue
Normal file
764
web/pages/admin/meilisearch-management.vue
Normal file
@@ -0,0 +1,764 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 页面标题 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">搜索优化管理</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">管理 Meilisearch 搜索服务状态和数据同步</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<n-button @click="refreshStatus" :loading="refreshing" :disabled="syncProgress.is_running">
|
||||
<template #icon>
|
||||
<i class="fas fa-refresh"></i>
|
||||
</template>
|
||||
刷新状态
|
||||
</n-button>
|
||||
<n-button @click="navigateTo('/admin/feature-config')" type="info" :disabled="syncProgress.is_running">
|
||||
<template #icon>
|
||||
<i class="fas fa-cog"></i>
|
||||
</template>
|
||||
配置设置
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态卡片 -->
|
||||
<n-card class="mb-6">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<!-- 启用状态 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-power-off text-sm" :class="status.enabled ? 'text-green-500' : 'text-red-500'"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">启用状态</p>
|
||||
<p class="text-sm font-medium" :class="status.enabled ? 'text-green-600' : 'text-red-600'">
|
||||
{{ status.enabled ? '已启用' : '未启用' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 健康状态 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-heartbeat text-sm" :class="status.healthy ? 'text-green-500' : 'text-red-500'"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">健康状态</p>
|
||||
<p class="text-sm font-medium" :class="status.healthy ? 'text-green-600' : 'text-red-600'">
|
||||
{{ status.healthy ? '正常' : '异常' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文档数量 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-database text-sm text-blue-500"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">索引文档</p>
|
||||
<p class="text-sm font-medium text-blue-600">{{ status.documentCount || 0 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最后检查时间 -->
|
||||
<div class="flex items-center space-x-2 p-2 bg-gray-50 dark:bg-gray-800 rounded">
|
||||
<i class="fas fa-clock text-sm text-purple-500"></i>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">最后检查</p>
|
||||
<p class="text-xs font-medium text-purple-600">{{ formatTime(status.lastCheck) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="status.lastError" class="mt-3 p-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded">
|
||||
<div class="flex items-start space-x-2">
|
||||
<i class="fas fa-exclamation-triangle text-red-500 mt-0.5 text-sm"></i>
|
||||
<div>
|
||||
<p class="text-xs font-medium text-red-800 dark:text-red-200">错误信息</p>
|
||||
<p class="text-xs text-red-700 dark:text-red-300">{{ status.lastError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 数据同步管理 -->
|
||||
<n-card class="mb-6">
|
||||
<div class="space-y-4">
|
||||
<!-- 标题、过滤条件和操作按钮 -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-lg font-semibold">资源列表</h4>
|
||||
<div class="flex space-x-3">
|
||||
<n-button
|
||||
type="primary"
|
||||
@click="syncAllResources"
|
||||
:loading="syncing"
|
||||
:disabled="unsyncedCount === 0 || syncProgress.is_running"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-upload"></i>
|
||||
</template>
|
||||
同步所有资源
|
||||
</n-button>
|
||||
<!-- 停止同步按钮已隐藏 -->
|
||||
<n-button
|
||||
type="error"
|
||||
@click="clearIndex"
|
||||
:loading="clearing"
|
||||
:disabled="syncProgress.is_running"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-trash"></i>
|
||||
</template>
|
||||
清空索引
|
||||
</n-button>
|
||||
<!-- <n-button
|
||||
type="info"
|
||||
@click="updateIndexSettings"
|
||||
:loading="updatingSettings"
|
||||
:disabled="syncProgress.is_running"
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="fas fa-cogs"></i>
|
||||
</template>
|
||||
更新索引设置
|
||||
</n-button> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 过滤条件 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">同步状态:</span>
|
||||
<n-select
|
||||
v-model:value="syncFilter"
|
||||
:options="syncFilterOptions"
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
:disabled="syncProgress.is_running"
|
||||
@update:value="onSyncFilterChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">总计: {{ totalCount }} 个</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 同步进度显示 -->
|
||||
<div v-if="syncProgress.is_running" class="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h5 class="text-sm font-medium text-blue-800 dark:text-blue-200">同步进度</h5>
|
||||
<span class="text-xs text-blue-600 dark:text-blue-300">
|
||||
批次 {{ syncProgress.current_batch }}/{{ syncProgress.total_batches }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="w-full bg-blue-200 dark:bg-blue-800 rounded-full h-2 mb-2">
|
||||
<div
|
||||
class="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||
:style="{ width: progressPercentage + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 进度信息 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">已同步:</span>
|
||||
<span class="font-medium">{{ syncProgress.synced_count }}/{{ syncProgress.total_count }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">进度:</span>
|
||||
<span class="font-medium">{{ progressPercentage.toFixed(1) }}%</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">预估剩余:</span>
|
||||
<span class="font-medium">{{ syncProgress.estimated_time || '计算中...' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-blue-600 dark:text-blue-300">开始时间:</span>
|
||||
<span class="font-medium">{{ formatTime(syncProgress.start_time) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误信息 -->
|
||||
<div v-if="syncProgress.error_message" class="mt-2 p-2 bg-red-100 dark:bg-red-900/20 rounded text-xs text-red-700 dark:text-red-300">
|
||||
<i class="fas fa-exclamation-triangle mr-1"></i>
|
||||
{{ syncProgress.error_message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div v-if="resources.length > 0">
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="resources"
|
||||
:pagination="pagination"
|
||||
:max-height="400"
|
||||
virtual-scroll
|
||||
:loading="loadingResources"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="!loadingResources" class="text-center py-8 text-gray-500">
|
||||
暂无资源数据
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useNotification, useDialog } from 'naive-ui'
|
||||
|
||||
// 设置页面布局
|
||||
definePageMeta({
|
||||
layout: 'admin',
|
||||
ssr: false
|
||||
})
|
||||
|
||||
const notification = useNotification()
|
||||
const dialog = useDialog()
|
||||
|
||||
// 状态数据
|
||||
const status = ref({
|
||||
enabled: false,
|
||||
healthy: false,
|
||||
documentCount: 0,
|
||||
lastCheck: null as Date | null,
|
||||
lastError: '',
|
||||
errorCount: 0
|
||||
})
|
||||
|
||||
const systemConfig = ref({
|
||||
meilisearch_host: '',
|
||||
meilisearch_port: '',
|
||||
meilisearch_master_key: '',
|
||||
meilisearch_index_name: ''
|
||||
})
|
||||
|
||||
// 定义资源类型
|
||||
interface Resource {
|
||||
id: number
|
||||
title: string
|
||||
category?: {
|
||||
name: string
|
||||
}
|
||||
synced_to_meilisearch: boolean
|
||||
synced_at?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// 同步状态过滤选项
|
||||
const syncFilterOptions = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '已同步', value: 'synced' },
|
||||
{ label: '未同步', value: 'unsynced' }
|
||||
]
|
||||
|
||||
const syncFilter = ref('unsynced') // 默认显示未同步
|
||||
const totalCount = ref(0)
|
||||
const resources = ref<Resource[]>([])
|
||||
const unsyncedCount = ref(0)
|
||||
|
||||
// 加载状态
|
||||
const refreshing = ref(false)
|
||||
const syncing = ref(false)
|
||||
const clearing = ref(false)
|
||||
const updatingSettings = ref(false)
|
||||
const loadingResources = ref(false)
|
||||
const stopping = ref(false)
|
||||
|
||||
// 同步进度
|
||||
const syncProgress = ref({
|
||||
is_running: false,
|
||||
total_count: 0,
|
||||
processed_count: 0,
|
||||
synced_count: 0,
|
||||
failed_count: 0,
|
||||
start_time: null as Date | null,
|
||||
estimated_time: '',
|
||||
current_batch: 0,
|
||||
total_batches: 0,
|
||||
error_message: ''
|
||||
})
|
||||
|
||||
// 计算进度百分比
|
||||
const progressPercentage = computed(() => {
|
||||
if (syncProgress.value.total_count === 0) return 0
|
||||
return (syncProgress.value.synced_count / syncProgress.value.total_count) * 100
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
itemCount: 0,
|
||||
showSizePicker: true,
|
||||
pageSizes: [500, 1000, 2000],
|
||||
onChange: (page: number) => {
|
||||
pagination.value.page = page
|
||||
fetchResources()
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.value.pageSize = pageSize
|
||||
pagination.value.page = 1
|
||||
fetchResources()
|
||||
}
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '标题',
|
||||
key: 'title',
|
||||
ellipsis: {
|
||||
tooltip: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
key: 'category',
|
||||
width: 120,
|
||||
render: (row: Resource) => {
|
||||
return row.category?.name || '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '同步状态',
|
||||
key: 'synced_to_meilisearch',
|
||||
width: 100,
|
||||
render: (row: Resource) => {
|
||||
return row.synced_to_meilisearch ? '已同步' : '未同步'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '同步时间',
|
||||
key: 'synced_at',
|
||||
width: 180,
|
||||
render: (row: Resource) => {
|
||||
return row.synced_at ? formatTime(row.synced_at) : '-'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
render: (row: Resource) => {
|
||||
return formatTime(row.created_at)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: Date | string | null) => {
|
||||
if (!time) return '未知'
|
||||
const date = new Date(time)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取状态
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const response = await meilisearchApi.getStatus() as any
|
||||
|
||||
if (response) {
|
||||
status.value = {
|
||||
enabled: response.enabled || false,
|
||||
healthy: response.healthy || false,
|
||||
documentCount: response.document_count || response.documentCount || 0,
|
||||
lastCheck: response.last_check ? new Date(response.last_check) : response.lastCheck ? new Date(response.lastCheck) : null,
|
||||
lastError: response.last_error || response.lastError || '',
|
||||
errorCount: response.error_count || response.errorCount || 0
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取状态失败:', error)
|
||||
notification.error({
|
||||
content: `获取状态失败: ${error?.message || error}`,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统配置
|
||||
const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const { useSystemConfigApi } = await import('~/composables/useApi')
|
||||
const systemConfigApi = useSystemConfigApi()
|
||||
const response = await systemConfigApi.getSystemConfig() as any
|
||||
|
||||
if (response) {
|
||||
systemConfig.value = {
|
||||
meilisearch_host: response.meilisearch_host || '',
|
||||
meilisearch_port: response.meilisearch_port || '',
|
||||
meilisearch_master_key: response.meilisearch_master_key || '',
|
||||
meilisearch_index_name: response.meilisearch_index_name || ''
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未同步数量
|
||||
const fetchUnsyncedCount = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const response = await meilisearchApi.getUnsyncedCount() as any
|
||||
|
||||
if (response) {
|
||||
unsyncedCount.value = response.count || 0
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取未同步数量失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态
|
||||
const refreshStatus = async () => {
|
||||
refreshing.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchStatus(),
|
||||
fetchSystemConfig(),
|
||||
fetchUnsyncedCount()
|
||||
])
|
||||
notification.success({
|
||||
content: '状态刷新成功',
|
||||
duration: 2000
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('刷新状态失败:', error)
|
||||
} finally {
|
||||
refreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 同步所有资源
|
||||
const syncAllResources = async () => {
|
||||
syncing.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.syncAllResources()
|
||||
|
||||
notification.success({
|
||||
content: '同步已开始,请查看进度',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 开始轮询进度
|
||||
startProgressPolling()
|
||||
} catch (error: any) {
|
||||
console.error('同步资源失败:', error)
|
||||
notification.error({
|
||||
content: `同步资源失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
syncing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停止同步
|
||||
const stopSync = async () => {
|
||||
stopping.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.stopSync()
|
||||
|
||||
notification.success({
|
||||
content: '同步已停止',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 立即更新进度状态为已停止
|
||||
syncProgress.value.is_running = false
|
||||
syncProgress.value.error_message = '同步已停止'
|
||||
|
||||
// 停止轮询
|
||||
stopProgressPolling()
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus()
|
||||
} catch (error: any) {
|
||||
console.error('停止同步失败:', error)
|
||||
notification.error({
|
||||
content: `停止同步失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
stopping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 进度轮询
|
||||
let progressInterval: NodeJS.Timeout | null = null
|
||||
|
||||
const startProgressPolling = () => {
|
||||
// 清除之前的轮询
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval)
|
||||
}
|
||||
|
||||
// 立即获取一次进度
|
||||
fetchSyncProgress()
|
||||
|
||||
// 每2秒轮询一次
|
||||
progressInterval = setInterval(() => {
|
||||
fetchSyncProgress()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
const stopProgressPolling = () => {
|
||||
if (progressInterval) {
|
||||
clearInterval(progressInterval)
|
||||
progressInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const fetchSyncProgress = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const progress = await meilisearchApi.getSyncProgress() as any
|
||||
|
||||
if (progress) {
|
||||
syncProgress.value = {
|
||||
is_running: progress.is_running || false,
|
||||
total_count: progress.total_count || 0,
|
||||
processed_count: progress.processed_count || 0,
|
||||
synced_count: progress.synced_count || 0,
|
||||
failed_count: progress.failed_count || 0,
|
||||
start_time: progress.start_time ? new Date(progress.start_time) : null,
|
||||
estimated_time: progress.estimated_time || '',
|
||||
current_batch: progress.current_batch || 0,
|
||||
total_batches: progress.total_batches || 0,
|
||||
error_message: progress.error_message || ''
|
||||
}
|
||||
|
||||
// 如果同步完成或出错,停止轮询
|
||||
if (!progress.is_running) {
|
||||
stopProgressPolling()
|
||||
|
||||
// 只有在有同步进度时才显示完成消息
|
||||
if (progress.synced_count > 0 || progress.error_message) {
|
||||
if (progress.error_message) {
|
||||
notification.error({
|
||||
content: `同步失败: ${progress.error_message}`,
|
||||
duration: 5000
|
||||
})
|
||||
} else {
|
||||
notification.success({
|
||||
content: `同步完成,共同步 ${progress.synced_count} 个资源`,
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新状态和表格
|
||||
await Promise.all([
|
||||
refreshStatus(),
|
||||
fetchResources()
|
||||
])
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取同步进度失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 静默获取同步进度,不显示任何提示
|
||||
const fetchSyncProgressSilent = async () => {
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
const progress = await meilisearchApi.getSyncProgress() as any
|
||||
|
||||
if (progress) {
|
||||
syncProgress.value = {
|
||||
is_running: progress.is_running || false,
|
||||
total_count: progress.total_count || 0,
|
||||
processed_count: progress.processed_count || 0,
|
||||
synced_count: progress.synced_count || 0,
|
||||
failed_count: progress.failed_count || 0,
|
||||
start_time: progress.start_time ? new Date(progress.start_time) : null,
|
||||
estimated_time: progress.estimated_time || '',
|
||||
current_batch: progress.current_batch || 0,
|
||||
total_batches: progress.total_batches || 0,
|
||||
error_message: progress.error_message || ''
|
||||
}
|
||||
|
||||
// 如果同步完成或出错,停止轮询
|
||||
if (!progress.is_running) {
|
||||
stopProgressPolling()
|
||||
|
||||
// 静默刷新状态和表格,不显示任何提示
|
||||
await Promise.all([
|
||||
refreshStatus(),
|
||||
fetchResources()
|
||||
])
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取同步进度失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 同步状态过滤变化处理
|
||||
const onSyncFilterChange = () => {
|
||||
pagination.value.page = 1
|
||||
fetchResources()
|
||||
}
|
||||
|
||||
// 获取资源列表
|
||||
const fetchResources = async () => {
|
||||
loadingResources.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
|
||||
let response: any
|
||||
if (syncFilter.value === 'unsynced') {
|
||||
// 获取未同步资源
|
||||
response = await meilisearchApi.getUnsyncedResources({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
} else if (syncFilter.value === 'synced') {
|
||||
// 获取已同步资源
|
||||
response = await meilisearchApi.getSyncedResources({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
} else {
|
||||
// 获取所有资源
|
||||
response = await meilisearchApi.getAllResources({
|
||||
page: pagination.value.page,
|
||||
page_size: pagination.value.pageSize
|
||||
})
|
||||
}
|
||||
|
||||
if (response && response.resources) {
|
||||
resources.value = response.resources
|
||||
totalCount.value = response.total || 0
|
||||
// 更新分页信息
|
||||
if (response.total !== undefined) {
|
||||
pagination.value.itemCount = response.total
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取资源失败:', error)
|
||||
notification.error({
|
||||
content: `获取资源失败: ${error?.message || error}`,
|
||||
duration: 3000
|
||||
})
|
||||
} finally {
|
||||
loadingResources.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取未同步资源(保留兼容性)
|
||||
const fetchUnsyncedResources = async () => {
|
||||
syncFilter.value = 'unsynced'
|
||||
await fetchResources()
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
const clearIndex = async () => {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
dialog.error({
|
||||
title: '确认清空索引',
|
||||
content: '此操作将清空所有 Meilisearch 索引数据,确定要继续吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: resolve,
|
||||
onNegativeClick: reject
|
||||
})
|
||||
})
|
||||
|
||||
clearing.value = true
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.clearIndex()
|
||||
|
||||
notification.success({
|
||||
content: '索引清空成功',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus()
|
||||
} catch (error: any) {
|
||||
if (error) {
|
||||
console.error('清空索引失败:', error)
|
||||
notification.error({
|
||||
content: `清空索引失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
clearing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新索引设置
|
||||
const updateIndexSettings = async () => {
|
||||
updatingSettings.value = true
|
||||
try {
|
||||
const { useMeilisearchApi } = await import('~/composables/useApi')
|
||||
const meilisearchApi = useMeilisearchApi()
|
||||
await meilisearchApi.updateIndexSettings()
|
||||
|
||||
notification.success({
|
||||
content: '索引设置已更新',
|
||||
duration: 3000
|
||||
})
|
||||
|
||||
// 刷新状态
|
||||
await refreshStatus()
|
||||
} catch (error: any) {
|
||||
console.error('更新索引设置失败:', error)
|
||||
notification.error({
|
||||
content: `更新索引设置失败: ${error?.message || error}`,
|
||||
duration: 5000
|
||||
})
|
||||
} finally {
|
||||
updatingSettings.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
refreshStatus()
|
||||
fetchResources()
|
||||
// 静默检查同步进度,不显示任何提示
|
||||
fetchSyncProgressSilent().then(() => {
|
||||
// 如果检测到有同步在进行,开始轮询
|
||||
if (syncProgress.value.is_running) {
|
||||
startProgressPolling()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 页面卸载时清理轮询
|
||||
onUnmounted(() => {
|
||||
stopProgressPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 自定义样式 */
|
||||
</style>
|
||||
@@ -149,10 +149,9 @@
|
||||
<div class="flex items-start">
|
||||
<span class="mr-2 flex-shrink-0" v-html="getPlatformIcon(resource.pan_id || 0)"></span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="break-words font-medium">{{ resource.title }}</div>
|
||||
<div class="break-words font-medium" v-html="resource.title_highlight || resource.title"></div>
|
||||
<!-- 显示描述 -->
|
||||
<div v-if="resource.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 break-words line-clamp-2">
|
||||
{{ resource.description }}
|
||||
<div v-if="resource.description_highlight || resource.description" class="text-xs text-gray-600 dark:text-gray-400 mt-1 break-words line-clamp-2" v-html="resource.description_highlight || resource.description">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -273,12 +272,24 @@ const handleLogoError = (event: Event) => {
|
||||
// 使用 useAsyncData 获取资源数据
|
||||
const { data: resourcesData, pending, refresh } = await useAsyncData(
|
||||
() => `resources-1-${route.query.search || ''}-${route.query.platform || ''}`,
|
||||
() => resourceApi.getResources({
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
search: route.query.search as string || '',
|
||||
pan_id: route.query.platform as string || ''
|
||||
})
|
||||
async () => {
|
||||
// 如果有搜索关键词,使用带搜索参数的资源接口(后端会优先使用Meilisearch)
|
||||
if (route.query.search) {
|
||||
return await resourceApi.getResources({
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
search: route.query.search as string,
|
||||
pan_id: route.query.platform as string || ''
|
||||
})
|
||||
} else {
|
||||
// 没有搜索关键词时,使用普通资源接口获取最新数据
|
||||
return await resourceApi.getResources({
|
||||
page: 1,
|
||||
page_size: 200,
|
||||
pan_id: route.query.platform as string || ''
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 获取统计数据
|
||||
@@ -399,8 +410,8 @@ onMounted(() => {
|
||||
|
||||
|
||||
// 获取平台名称
|
||||
const getPlatformIcon = (panId: string) => {
|
||||
const platform = (platforms.value as any).find((p: any) => p.id === panId)
|
||||
const getPlatformIcon = (panId: string | number) => {
|
||||
const platform = (platforms.value as any).find((p: any) => p.id == panId)
|
||||
return platform?.icon || '未知平台'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user