diff --git a/es/es_config.go b/es/es_config.go
new file mode 100644
index 0000000..8fe5f76
--- /dev/null
+++ b/es/es_config.go
@@ -0,0 +1,170 @@
+package es
+
+// ESFieldConfig ES 字段配置
+type ESFieldConfig struct {
+ // AllowUpdate 允许更新的字段列表
+ AllowUpdate map[string]bool
+
+ // AllowAdd 允许新增时填充的字段列表
+ AllowAdd map[string]bool
+
+ // FieldMappings 字段映射(Go 结构体字段名 -> ES 字段名)
+ FieldMappings map[string]string
+}
+
+// GetESFieldConfig 获取 ES 字段配置
+func GetESFieldConfig() *ESFieldConfig {
+ return &ESFieldConfig{
+ AllowUpdate: map[string]bool{
+ "book_pic": true,
+ "book_pic_s": true,
+ "book_pic_b": true,
+ "book_pic_w": true,
+ "book_def_pic": true,
+ "isbn": true,
+ "author": true,
+ "category": true,
+ "publisher": true,
+ "publication_time": true,
+ "binding_layout": true,
+ "fix_price": true,
+ "content": true,
+ "is_suit": true,
+ "day_sale_7": true,
+ "day_sale_15": true,
+ "day_sale_30": true,
+ "day_sale_60": true,
+ "day_sale_90": true,
+ "day_sale_180": true,
+ "day_sale_365": true,
+ "this_year_sale": true,
+ "last_year_sale": true,
+ "total_sale": true,
+ "buy_counts": true,
+ "sell_counts": true,
+ "is_illegal": true,
+ "is_return": true,
+ "is_filter": true,
+ "update_time": true,
+ "page_count": true,
+ "word_count": true,
+ "book_format": true,
+ "cat_id": true,
+ "other": true, // 允许更新 other 字段
+ },
+
+ AllowAdd: map[string]bool{
+ "id": true,
+ "book_name": true,
+ "book_pic": true,
+ "book_pic_s": true,
+ "book_pic_b": true,
+ "book_pic_w": true,
+ "isbn": true,
+ "author": true,
+ "category": true,
+ "publisher": true,
+ "publication_time": true,
+ "binding_layout": true,
+ "fix_price": true,
+ "content": true,
+ "is_suit": true,
+ "day_sale_7": true,
+ "day_sale_15": true,
+ "day_sale_30": true,
+ "day_sale_60": true,
+ "day_sale_90": true,
+ "day_sale_180": true,
+ "day_sale_365": true,
+ "this_year_sale": true,
+ "last_year_sale": true,
+ "total_sale": true,
+ "buy_counts": true,
+ "sell_counts": true,
+ "book_pic_obj": true,
+ "book_pic_obj_s": true,
+ "update_time": true,
+ "is_illegal": true,
+ "is_return": true,
+ "is_filter": true,
+ "page_count": true,
+ "word_count": true,
+ "book_format": true,
+ "other": true, // 允许新增 other 字段
+ },
+
+ FieldMappings: map[string]string{
+ "ID": "id",
+ "BookName": "book_name",
+ "BookPic": "book_pic",
+ "BookPicS": "book_pic_s",
+ "BookPicB": "book_pic_b",
+ "BookPicW": "book_pic_w",
+ "ISBN": "isbn",
+ "Author": "author",
+ "Category": "category",
+ "Publisher": "publisher",
+ "PublicationTime": "publication_time",
+ "BindingLayout": "binding_layout",
+ "FixPrice": "fix_price",
+ "Content": "content",
+ "IsSuit": "is_suit",
+ "DaySale7": "day_sale_7",
+ "DaySale15": "day_sale_15",
+ "DaySale30": "day_sale_30",
+ "DaySale60": "day_sale_60",
+ "DaySale90": "day_sale_90",
+ "DaySale180": "day_sale_180",
+ "DaySale365": "day_sale_365",
+ "ThisYearSale": "this_year_sale",
+ "LastYearSale": "last_year_sale",
+ "TotalSale": "total_sale",
+ "BuyCounts": "buy_counts",
+ "SellCounts": "sell_counts",
+ "BookPicObj": "book_pic_obj",
+ "BookPicObjS": "book_pic_obj_s",
+ "UpdateTime": "update_time",
+ "IsIllegal": "is_illegal",
+ "IsReturn": "is_return",
+ "IsFilter": "is_filter",
+ "PageCount": "page_count",
+ "WordCount": "word_count",
+ "BookFormat": "book_format",
+ "Other": "other",
+ },
+ }
+}
+
+// IsAllowUpdate 检查字段是否允许更新
+func (cfg *ESFieldConfig) IsAllowUpdate(field string) bool {
+ return cfg.AllowUpdate[field]
+}
+
+// IsAllowAdd 检查字段是否允许新增
+func (cfg *ESFieldConfig) IsAllowAdd(field string) bool {
+ return cfg.AllowAdd[field]
+}
+
+// GetESFieldName 获取 ES 字段名
+func (cfg *ESFieldConfig) GetESFieldName(goFieldName string) string {
+ if esName, ok := cfg.FieldMappings[goFieldName]; ok {
+ return esName
+ }
+ return goFieldName
+}
+
+// BuildESDocument 构建 ES 文档
+func (cfg *ESFieldConfig) BuildESDocument(data map[string]interface{}) map[string]interface{} {
+ doc := make(map[string]interface{})
+
+ for goField, value := range data {
+ // 获取 ES 字段名
+ esField := cfg.GetESFieldName(goField)
+ // 检查是否允许添加
+ if !cfg.IsAllowAdd(esField) {
+ continue
+ }
+ doc[esField] = value
+ }
+ return doc
+}
diff --git a/es/es_search.go b/es/es_search.go
index 09aaef6..3f7f843 100644
--- a/es/es_search.go
+++ b/es/es_search.go
@@ -6,11 +6,14 @@ import (
"centerBook/image"
"centerBook/kongfz"
"centerBook/model/request"
+ "centerBook/monitor"
"centerBook/tail"
"centerBook/util/redisClient"
"context"
"encoding/json"
"fmt"
+ "github.com/gin-gonic/gin"
+ jsoniter "github.com/json-iterator/go"
"io"
"log"
"net/http"
@@ -18,9 +21,6 @@ import (
"strings"
"time"
- "github.com/gin-gonic/gin"
- jsoniter "github.com/json-iterator/go"
-
"github.com/elastic/go-elasticsearch/v8/esapi"
)
@@ -59,9 +59,14 @@ type ESBookResponse struct {
BookPicObj map[string]interface{} `json:"book_pic_obj"`
BookPicObjS map[string]interface{} `json:"book_pic_obj_s"`
UpdateTime NumberOrString `json:"update_time"`
- IsIllegal int `json:"is_illegal"` // 是否非法 示例 000000
- IsReturn int `json:"is_return"` // 是否为驳回 示例 0 否 1 是
- IsFilter string `json:"is_filter"` // 过滤字段
+ IsIllegal int `json:"is_illegal"` // 是否非法 示例 000000
+ IsReturn int `json:"is_return"` // 是否为驳回 示例 0 否 1 是
+ IsFilter string `json:"is_filter"` // 过滤字段
+ PageCount NumberOrString `json:"page_count"` // 页数
+ WordCount NumberOrString `json:"word_count"` // 字数
+ BookFormat NumberOrString `json:"book_format"` // 多少开
+ //CatId request.CatIdObject `json:"cat_id"` // 类目
+ Other map[string]interface{} `json:"other"` // 扩展字段,用于兼容未来新增字段
}
// FlexibleString 处理可能是字符串或数组的字段
@@ -188,6 +193,11 @@ func (book *ESBook) ConvertToResponse() ESBookResponse {
IsIllegal: book.IsIllegal,
IsReturn: book.IsReturn,
IsFilter: book.IsFilter,
+ PageCount: book.PageCount,
+ WordCount: book.WordCount,
+ BookFormat: book.BookFormat,
+ //CatId: book.CatId,
+ Other: book.Other,
}
}
@@ -250,6 +260,7 @@ type ESBook struct {
WordCount NumberOrString `json:"word_count"` // 字数
BookFormat NumberOrString `json:"book_format"` // 多少开
CatId request.CatIdObject `json:"cat_id"` // 类目
+ Other map[string]interface{} `json:"other"` // 扩展字段,用于兼容未来新增字段
}
// AddBookRequest 用于 Service 方法的入参
@@ -459,7 +470,7 @@ type esHitsWrapper struct {
}
// SearchBooks 搜索图书
-func (svc *ESSearchService) SearchBooks(keyword string) ([]ESBook, error) {
+func (svc *ESSearchService) SearchBooks(keyword string, endpoint ...string) ([]ESBook, error) {
keyword = strings.TrimSpace(keyword)
if keyword == "" {
@@ -494,7 +505,18 @@ func (svc *ESSearchService) SearchBooks(keyword string) ([]ESBook, error) {
TrackTotalHits: true,
}
- res, err := req.Do(context.Background(), svc.ES.Client.Transport)
+ // 如果有传入 endpoint,使用监控
+ var res *esapi.Response
+ var duration time.Duration
+
+ if len(endpoint) > 0 && endpoint[0] != "" {
+ monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint[0])
+ res, duration, err = monitoredES.Search(context.Background(), &req)
+ log.Printf("[SearchBooks] ES 查询耗时:%dms", duration.Milliseconds())
+ } else {
+ // 原有逻辑,不带监控
+ res, err = req.Do(context.Background(), svc.ES.Client.Transport)
+ }
if err != nil {
return nil, fmt.Errorf("执行 ES 查询失败: %v", err)
}
@@ -1918,20 +1940,41 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
fmt.Printf("[DEBUG] ES Query Body:\n%s\n", string(body))
// ========== 执行 ES 查询 ==========
- res, err := svc.ES.Client.Search(
- svc.ES.Client.Search.WithIndex(ESIndex),
- svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
- svc.ES.Client.Search.WithTrackTotalHits(true),
- )
+ //res, err := svc.ES.Client.Search(
+ // svc.ES.Client.Search.WithIndex(ESIndex),
+ // svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
+ // svc.ES.Client.Search.WithTrackTotalHits(true),
+ //)
+ //
+ //fmt.Printf("[DEBUG] ES Query Response:\n%s\n", res)
+ //
+ //if err != nil {
+ // fmt.Printf("[ERROR] ES.Client.Search error: %v\n", err)
+ // return nil, 0, err
+ //}
+ //defer res.Body.Close()
+
+ endpoint := c.FullPath()
+ monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint)
+
+ queryBody := string(body)
+ req := esapi.SearchRequest{
+ Index: []string{ESIndex},
+ Body: bytes.NewReader([]byte(queryBody)),
+ TrackTotalHits: true,
+ Pretty: true,
+ }
+
+ res, duration, err := monitoredES.Search(context.Background(), &req)
fmt.Printf("[DEBUG] ES Query Response:\n%s\n", res)
+ fmt.Printf("[DEBUG] ES 查询耗时:%dms\n", duration.Milliseconds())
if err != nil {
fmt.Printf("[ERROR] ES.Client.Search error: %v\n", err)
return nil, 0, err
}
defer res.Body.Close()
-
// 读取响应
var buf bytes.Buffer
// 使用CopyBuffer可以重用缓冲区
@@ -2044,25 +2087,27 @@ func (svc *ESSearchService) BatchGetBookBaseInfoESHandler(c *gin.Context) {
"total": total,
})
}
-
func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) {
isbn := c.Query("isbn")
if isbn == "" {
+ log.Printf("[SearchBooksHandler] 缺少 isbn 参数")
c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
return
}
+ log.Printf("[SearchBooksHandler] ISBN 模糊搜索:%s", isbn)
ctx := context.Background()
+ endpoint := c.FullPath()
- db4Client, err := redisClient.GetClientByName("db1")
+ // 获取监控的 Redis 客户端
+ db1Client, err := redisClient.GetClientByName("db1")
if err == nil {
- val, err := db4Client.Get(ctx, isbn).Result()
+ monitoredRedis := monitor.NewMonitoredRedisClient(db1Client, endpoint)
+ val, _, err := monitoredRedis.Get(ctx, isbn)
if err == nil && val != "" {
- log.Printf("[SearchBooksHandler] 从 Redis db1 查询到数据: %s", isbn)
- // 使用 RedisBookInfo 结构体解析
+ log.Printf("[SearchBooksHandler] 从 Redis db1 查询到数据:%s", isbn)
var redisBook request.BookInfo
if err := json.Unmarshal([]byte(val), &redisBook); err == nil {
- // 转换为 ESBook
esBook := ConvertRedisBookToESBook(&redisBook)
if esBook != nil {
responseData := esBook.ConvertToResponse()
@@ -2071,13 +2116,12 @@ func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) {
})
return
}
- } else {
- log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err)
}
}
}
- result, err := svc.SearchBooks(isbn)
+ // ES 查询
+ result, err := svc.SearchBooks(isbn, endpoint)
if err != nil {
c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()})
return
@@ -2105,20 +2149,17 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn)
ctx := context.Background()
+ endpoint := c.FullPath()
- db4Client, err := redisClient.GetClientByName("db1")
- fmt.Println(db4Client)
-
- if err != nil {
- log.Printf("[SearchBookByISBNHandler] 获取 Redis db1 客户端失败: %v", err)
- } else {
- val, err := db4Client.Get(ctx, isbn).Result()
+ // Redis 查询(使用监控)
+ db1Client, err := redisClient.GetClientByName("db1")
+ if err == nil {
+ monitoredRedis := monitor.NewMonitoredRedisClient(db1Client, endpoint)
+ val, _, err := monitoredRedis.Get(ctx, isbn)
if err == nil && val != "" {
- log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据: %s", isbn)
- // 使用 RedisBookInfo 结构体解析
+ log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据:%s", isbn)
var redisBook request.BookInfo
if err := json.Unmarshal([]byte(val), &redisBook); err == nil {
- // 转换为 ESBook
esBook := ConvertRedisBookToESBook(&redisBook)
if esBook != nil {
responseData := esBook.ConvertToResponse()
@@ -2130,24 +2171,70 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
} else {
log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err)
}
- } else {
- log.Printf("[SearchBookByISBNHandler] Redis db1 中未找到 ISBN: %s", isbn)
}
}
- result, err := svc.SearchBookByISBN(isbn)
+ // ES 查询(使用监控)
+ query := map[string]interface{}{
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ "isbn": isbn,
+ },
+ },
+ "_source": true,
+ }
+
+ body, err := json.Marshal(query)
if err != nil {
- log.Printf("[SearchBookByISBNHandler] ES 查询失败: %v", err)
- c.JSON(500, gin.H{"error": err.Error()})
+ log.Printf("[SearchBookByISBNHandler] 构建查询 JSON 失败:%v", err)
+ c.JSON(500, gin.H{"error": "构建查询失败"})
return
}
+ req := esapi.SearchRequest{
+ Index: []string{ESIndex},
+ Body: bytes.NewReader(body),
+ TrackTotalHits: true,
+ Pretty: true,
+ }
+
+ // 创建监控客户端并执行查询
+ monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint)
+ resp, duration, err := monitoredES.Search(ctx, &req)
+
+ log.Printf("[SearchBookByISBNHandler] ES 查询耗时:%dms", duration.Milliseconds())
+
+ if err != nil {
+ log.Printf("[SearchBookByISBNHandler] ES 查询失败:%v", err)
+ c.JSON(500, gin.H{"error": "ES 查询失败:" + err.Error()})
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.IsError() {
+ log.Printf("[SearchBookByISBNHandler] ES 返回错误:%s", resp.String())
+ c.JSON(500, gin.H{"error": "ES 返回错误:" + resp.String()})
+ return
+ }
+
+ var parsed esHitsWrapper
+ if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
+ log.Printf("[SearchBookByISBNHandler] 解析 ES 响应失败:%v", err)
+ c.JSON(500, gin.H{"error": "解析响应失败"})
+ return
+ }
+
+ var result *ESBook
+ if len(parsed.Hits.Hits) > 0 {
+ result = &parsed.Hits.Hits[0].Source
+ }
+
if result == nil {
log.Printf("[SearchBookByISBNHandler] ES 中未找到 ISBN: %s,从孔夫子抓取", isbn)
apiBook, err := kongfz.GetBookImageByISBN(isbn, "CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f")
if err != nil {
- log.Printf("[SearchBookByISBNHandler] 孔夫子 API 查询失败: %v", err)
+ log.Printf("[SearchBookByISBNHandler] 孔夫子 API 查询失败:%v", err)
c.JSON(500, gin.H{"error": err.Error()})
return
}
@@ -2157,16 +2244,16 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
return
}
- log.Printf("[SearchBookByISBNHandler] 获取到图书信息: %+v", apiBook.Data)
+ log.Printf("[SearchBookByISBNHandler] 获取到图书信息:%+v", apiBook.Data)
pddBookPicURL := ""
if apiBook.Data.BookPic != "" {
url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPic, isbn, "true", apiBook.Data.BookName, "true")
if err != nil {
- log.Printf("[SearchBookByISBNHandler] 上传 book_pic 失败: %v", err)
+ log.Printf("[SearchBookByISBNHandler] 上传 book_pic 失败:%v", err)
} else {
pddBookPicURL = url
- log.Printf("[SearchBookByISBNHandler] 上传 book_pic 成功: %s", url)
+ log.Printf("[SearchBookByISBNHandler] 上传 book_pic 成功:%s", url)
}
}
@@ -2174,10 +2261,10 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
if apiBook.Data.BookPicS != "" {
url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPicS, isbn, "true", apiBook.Data.BookName, "true")
if err != nil {
- log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 失败: %v", err)
+ log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 失败:%v", err)
} else {
pddBookPicSURL = url
- log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 成功: %s", url)
+ log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 成功:%s", url)
}
}
@@ -2185,18 +2272,15 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
esBook.BookPicS.PddResponse = pddBookPicSURL
esBook.BookPic.PddPath = pddBookPicURL
- //log.Printf("[SearchBookByISBNHandler] 写入 ES: %+v", esBook)
result, err = svc.AddBookToES(c.Request.Context(), esBook)
if err != nil {
- log.Printf("[SearchBookByISBNHandler] 写入 ES 失败: %v", err)
+ log.Printf("[SearchBookByISBNHandler] 写入 ES 失败:%v", err)
c.JSON(500, gin.H{"error": err.Error()})
return
}
- log.Printf("[SearchBookByISBNHandler] 写入 ES 成功, ISBN: %s", isbn)
- } else {
- //log.Printf("[SearchBookByISBNHandler] 从 ES 查询到图书: %+v", result)
+ log.Printf("[SearchBookByISBNHandler] 写入 ES 成功,ISBN: %s", isbn)
}
responseData := result.ConvertToResponse()
@@ -2205,6 +2289,166 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
})
}
+// func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) {
+// isbn := c.Query("isbn")
+// if isbn == "" {
+// c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
+// return
+// }
+//
+// ctx := context.Background()
+//
+// db4Client, err := redisClient.GetClientByName("db1")
+// if err == nil {
+// val, err := db4Client.Get(ctx, isbn).Result()
+// if err == nil && val != "" {
+// log.Printf("[SearchBooksHandler] 从 Redis db1 查询到数据: %s", isbn)
+// // 使用 RedisBookInfo 结构体解析
+// var redisBook request.BookInfo
+// if err := json.Unmarshal([]byte(val), &redisBook); err == nil {
+// // 转换为 ESBook
+// esBook := ConvertRedisBookToESBook(&redisBook)
+// if esBook != nil {
+// responseData := esBook.ConvertToResponse()
+// c.JSON(200, gin.H{
+// "data": responseData,
+// })
+// return
+// }
+// } else {
+// log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err)
+// }
+// }
+// }
+//
+// result, err := svc.SearchBooks(isbn)
+// if err != nil {
+// c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()})
+// return
+// }
+//
+// responseList := make([]ESBookResponse, 0, len(result))
+// for _, book := range result {
+// responseList = append(responseList, book.ConvertToResponse())
+// }
+//
+// c.JSON(200, gin.H{
+// "count": len(result),
+// "data": responseList,
+// })
+// }
+//func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
+//
+// isbn := c.Query("isbn")
+// if isbn == "" {
+// log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数")
+// c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
+// return
+// }
+//
+// log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn)
+//
+// ctx := context.Background()
+//
+// db4Client, err := redisClient.GetClientByName("db1")
+// fmt.Println(db4Client)
+//
+// if err != nil {
+// log.Printf("[SearchBookByISBNHandler] 获取 Redis db1 客户端失败: %v", err)
+// } else {
+// val, err := db4Client.Get(ctx, isbn).Result()
+// if err == nil && val != "" {
+// log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据: %s", isbn)
+// // 使用 RedisBookInfo 结构体解析
+// var redisBook request.BookInfo
+// if err := json.Unmarshal([]byte(val), &redisBook); err == nil {
+// // 转换为 ESBook
+// esBook := ConvertRedisBookToESBook(&redisBook)
+// if esBook != nil {
+// responseData := esBook.ConvertToResponse()
+// c.JSON(200, gin.H{
+// "data": responseData,
+// })
+// return
+// }
+// } else {
+// log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err)
+// }
+// } else {
+// log.Printf("[SearchBookByISBNHandler] Redis db1 中未找到 ISBN: %s", isbn)
+// }
+// }
+//
+// result, err := svc.SearchBookByISBN(isbn)
+// if err != nil {
+// log.Printf("[SearchBookByISBNHandler] ES 查询失败: %v", err)
+// c.JSON(500, gin.H{"error": err.Error()})
+// return
+// }
+//
+// if result == nil {
+// log.Printf("[SearchBookByISBNHandler] ES 中未找到 ISBN: %s,从孔夫子抓取", isbn)
+//
+// apiBook, err := kongfz.GetBookImageByISBN(isbn, "CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f")
+// if err != nil {
+// log.Printf("[SearchBookByISBNHandler] 孔夫子 API 查询失败: %v", err)
+// c.JSON(500, gin.H{"error": err.Error()})
+// return
+// }
+// if apiBook == nil || apiBook.Data.ISBN == "" {
+// log.Printf("[SearchBookByISBNHandler] 孔夫子 API 未找到图书信息 ISBN: %s", isbn)
+// c.JSON(404, gin.H{"error": "未找到图书信息"})
+// return
+// }
+//
+// log.Printf("[SearchBookByISBNHandler] 获取到图书信息: %+v", apiBook.Data)
+//
+// pddBookPicURL := ""
+// if apiBook.Data.BookPic != "" {
+// url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPic, isbn, "true", apiBook.Data.BookName, "true")
+// if err != nil {
+// log.Printf("[SearchBookByISBNHandler] 上传 book_pic 失败: %v", err)
+// } else {
+// pddBookPicURL = url
+// log.Printf("[SearchBookByISBNHandler] 上传 book_pic 成功: %s", url)
+// }
+// }
+//
+// pddBookPicSURL := ""
+// if apiBook.Data.BookPicS != "" {
+// url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPicS, isbn, "true", apiBook.Data.BookName, "true")
+// if err != nil {
+// log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 失败: %v", err)
+// } else {
+// pddBookPicSURL = url
+// log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 成功: %s", url)
+// }
+// }
+//
+// esBook := ConvertKongfzToESBook(apiBook)
+//
+// esBook.BookPicS.PddResponse = pddBookPicSURL
+// esBook.BookPic.PddPath = pddBookPicURL
+// //log.Printf("[SearchBookByISBNHandler] 写入 ES: %+v", esBook)
+//
+// result, err = svc.AddBookToES(c.Request.Context(), esBook)
+// if err != nil {
+// log.Printf("[SearchBookByISBNHandler] 写入 ES 失败: %v", err)
+// c.JSON(500, gin.H{"error": err.Error()})
+// return
+// }
+//
+// log.Printf("[SearchBookByISBNHandler] 写入 ES 成功, ISBN: %s", isbn)
+// } else {
+// //log.Printf("[SearchBookByISBNHandler] 从 ES 查询到图书: %+v", result)
+// }
+//
+// responseData := result.ConvertToResponse()
+// c.JSON(200, gin.H{
+// "data": responseData,
+// })
+//}
+
// ConvertKongfzToESBook 将第三方接口返回的数据转换为 ESBook 结构
func ConvertKongfzToESBook(apiBook *kongfz.BookResponse) *ESBook {
if apiBook == nil || apiBook.Data.ISBN == "" {
diff --git a/health_api.go b/health_api.go
index 1cba748..c6bddcd 100644
--- a/health_api.go
+++ b/health_api.go
@@ -1,11 +1,10 @@
package main
import (
+ "github.com/gin-gonic/gin"
"net/http"
"strconv"
"time"
-
- "github.com/gin-gonic/gin"
)
// SQLHealthController SQL健康监控控制器
@@ -140,253 +139,253 @@ func (shc *SQLHealthController) GetSQLHealthDashboard(c *gin.Context) {
-
-
- SQL健康监控仪表板
-
+
+
+ SQL健康监控仪表板
+
-
-
SQL健康监控仪表板
-
-
-
-
-
-
-
-
+
+
SQL健康监控仪表板
-
+
+
+
+
+
+
+
-
+
-
+
-
-
+
-
+ let html = '
| ID | 查询语句 | 错误信息 | 时间 | 接口 |
';
+ failedQueries.forEach(query => {
+ html += ` + "`" + `
+ | ${query.id} |
+ ${query.query} |
+ ${query.error} |
+ ${new Date(query.timestamp).toLocaleString()} |
+ ${query.endpoint} |
+
` + "`" + `;
+ });
+ html += '
';
+ container.innerHTML = html;
+ })
+ .catch(error => console.error('加载失败查询失败:', error));
+ }
+
+ function clearRecords() {
+ if (confirm('确定要清空所有SQL记录吗?')) {
+ fetch('/api/sql-health/clear', { method: 'POST' })
+ .then(response => response.json())
+ .then(data => {
+ alert('记录已清空');
+ refreshData();
+ })
+ .catch(error => console.error('清空记录失败:', error));
+ }
+ }
+
+ function updateLimit() {
+ const limitInput = document.getElementById('limitInput');
+ const newLimit = parseInt(limitInput.value);
+ if (newLimit > 0 && newLimit <= 500) {
+ currentLimit = newLimit;
+ loadRecentRecords();
+ } else {
+ alert('记录数量必须在1-500之间');
+ }
+ }
+
+ function toggleAutoRefresh() {
+ const checkbox = document.getElementById('autoRefresh');
+ if (checkbox.checked) {
+ autoRefreshInterval = setInterval(refreshData, 10000);
+ } else {
+ clearInterval(autoRefreshInterval);
+ }
+ }
+
+ // 初始化
+ document.getElementById('autoRefresh').addEventListener('change', toggleAutoRefresh);
+ refreshData();
+ toggleAutoRefresh();
+
- `
+ `
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, dashboardHTML)
diff --git a/main.go b/main.go
index 6870c43..620de71 100644
--- a/main.go
+++ b/main.go
@@ -29,6 +29,7 @@ import (
"github.com/go-redis/redis/v8"
"centerBook/es"
+ "centerBook/monitor"
"centerBook/util/redisClient"
"github.com/gin-gonic/gin"
@@ -284,20 +285,27 @@ func main() {
//r.GET("/ready", bookCenter.ReadyCheck)
// 7. SQL健康监控端点
- sqlHealthController := NewSQLHealthController()
- r.GET("/api/sql-health/stats", sqlHealthController.GetSQLStats)
- r.GET("/api/sql-health/recent", sqlHealthController.GetRecentSQLRecords)
- r.GET("/api/sql-health/slow-queries", sqlHealthController.GetSlowQueries)
- r.GET("/api/sql-health/failed-queries", sqlHealthController.GetFailedQueries)
- r.POST("/api/sql-health/clear", sqlHealthController.ClearSQLRecords)
- r.GET("/api/sql-health/dashboard", sqlHealthController.GetSQLHealthDashboard)
-
+ //sqlHealthController := NewSQLHealthController()
+ //r.GET("/api/sql-health/stats", sqlHealthController.GetSQLStats)
+ //r.GET("/api/sql-health/recent", sqlHealthController.GetRecentSQLRecords)
+ //r.GET("/api/sql-health/slow-queries", sqlHealthController.GetSlowQueries)
+ //r.GET("/api/sql-health/failed-queries", sqlHealthController.GetFailedQueries)
+ //r.POST("/api/sql-health/clear", sqlHealthController.ClearSQLRecords)
+ //r.GET("/api/sql-health/dashboard", sqlHealthController.GetSQLHealthDashboard)
+ // 8. API 监控端点(ES 和 Redis 调用监控)
+ apiMonitorController := monitor.NewAPIMonitorController()
+ r.GET("/api/api-monitor/stats", apiMonitorController.GetAPIStats)
+ r.GET("/api/api-monitor/all-stats", apiMonitorController.GetAllAPIStats)
+ r.GET("/api/api-monitor/es-calls", apiMonitorController.GetAPIESCalls)
+ r.GET("/api/api-monitor/redis-calls", apiMonitorController.GetAPIRedisCalls)
+ r.GET("/api/api-monitor/dashboard", apiMonitorController.GetAPIMonitorDashboard)
+ r.GET("/api/api-monitor/call-detail", apiMonitorController.GetAPICallDetail)
// =================== ⭐ ES =====================
// ISBN 模糊搜索
- r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler)
+ r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler) //监控
// ISBN 精确搜索
- r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) //1
+ r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) //监控
// 书名搜索
r.GET("/api/es/searchByBookName", esService.SearchBookByBookNameHandler)
// 全字段搜索
@@ -312,7 +320,7 @@ func main() {
// 根据条件查询 ES 图书信息
r.GET("/api/es/getBookBaseInfoES", bookController.SearchBookBaseInfoHandler)
// 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新
- //r.POST("/api/es/addBookToES", bookController.AddBookToESHandler)
+ r.POST("/api/es/addBookToES", bookController.AddBookToESHandler)
// 更新:根据ISBN通用更新图书字段
r.POST("/api/es/updateBookFieldsByISBN", bookController.UpdateBookFieldsByISBNHandler)
// 更新:根据ISBN通用更新图书字段
@@ -324,7 +332,7 @@ func main() {
//------------------------------------------------------------------------
// 新:核价软件用批量获取
- r.GET("/api/es/batchGetBookBaseInfoES", esService.BatchGetBookBaseInfoESHandler)
+ r.GET("/api/es/batchGetBookBaseInfoES", esService.BatchGetBookBaseInfoESHandler) //监控
// 多条件高级搜索
r.GET("/api/es/searchAdvanced", esService.SearchBooksByConditionsHandler)
// ID 范围计数
@@ -338,7 +346,7 @@ func main() {
// 新增:根据ISBN通用更新图书字段
//r.POST("/api/es/updateBookFieldsByISBN", esService.UpdateBookFieldsByISBNHandler) //1
// 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新
- r.POST("/api/es/addBookToES", esService.AddBookToESHandler) //1
+ //r.POST("/api/es/addBookToES", esService.AddBookToESHandler) //1
// 新增:完整插入接口,支持所有字段,
r.POST("/api/es/addBookFullToES", esService.AddBookFullToESHandler)
// 新增:批量插入接口,支持同时插入多本图书
diff --git a/monitor/api_monitor.go b/monitor/api_monitor.go
new file mode 100644
index 0000000..ea359bd
--- /dev/null
+++ b/monitor/api_monitor.go
@@ -0,0 +1,774 @@
+package monitor
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "github.com/elastic/go-elasticsearch/v8"
+ "github.com/elastic/go-elasticsearch/v8/esapi"
+ "io"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/go-redis/redis/v8"
+)
+
+// APIMonitor API 监控器 - 监控特定接口的 ES 和 Redis 调用
+type APIMonitor struct {
+ endpoint string
+ esCalls []APICallRecord
+ redisCalls []APICallRecord
+ mutex sync.RWMutex
+ //maxRecords int
+ nextID int64
+ qpsWindow time.Duration // QPS 统计时间窗口
+}
+
+// APICallRecord API 调用记录
+type APICallRecord struct {
+ ID int64 `json:"id"`
+ Timestamp time.Time `json:"timestamp"`
+ Duration int64 `json:"duration_ms"`
+ Success bool `json:"success"`
+ Error string `json:"error,omitempty"`
+ Operation string `json:"operation"` // ES: search/get, Redis: GET/SET/HGET
+ KeyOrIndex string `json:"key_or_index"` // ES 索引或 Redis Key
+ Query string `json:"query,omitempty"` // 查询内容
+ Request string `json:"request,omitempty"` // 请求内容
+ Response string `json:"response,omitempty"` // 响应内容
+}
+
+// APIStats API 统计数据
+type APIStats struct {
+ Endpoint string `json:"endpoint"`
+ TotalCalls int64 `json:"total_calls"`
+ Escalls int64 `json:"es_calls"`
+ RedisCalls int64 `json:"redis_calls"`
+ ESQPS float64 `json:"es_qps"` // ES QPS
+ RedisQPS float64 `json:"redis_qps"` // Redis QPS
+ AvgDuration int64 `json:"avg_duration_ms"`
+ SuccessRate float64 `json:"success_rate"`
+ LastUpdate string `json:"last_update"`
+}
+
+// 全局监控器映射表:endpoint -> APIMonitor
+var (
+ apiMonitors = make(map[string]*APIMonitor)
+ apiMonitorsMu sync.RWMutex
+)
+
+// GetOrCreateAPIMonitor 获取或创建 API 监控器
+func GetOrCreateAPIMonitor(endpoint string, qpsWindow time.Duration) *APIMonitor {
+ apiMonitorsMu.Lock()
+ defer apiMonitorsMu.Unlock()
+
+ if monitor, exists := apiMonitors[endpoint]; exists {
+ return monitor
+ }
+
+ monitor := &APIMonitor{
+ endpoint: endpoint,
+ esCalls: make([]APICallRecord, 0),
+ redisCalls: make([]APICallRecord, 0),
+ //maxRecords: maxRecords,
+ nextID: 1,
+ qpsWindow: qpsWindow,
+ }
+ apiMonitors[endpoint] = monitor
+ return monitor
+}
+
+// RecordESCall 记录 ES 调用
+func (am *APIMonitor) RecordESCall(operation, keyOrIndex, query string, duration time.Duration, err error) {
+ am.mutex.Lock()
+ defer am.mutex.Unlock()
+
+ record := APICallRecord{
+ ID: am.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: operation,
+ KeyOrIndex: keyOrIndex,
+ Query: query,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ am.esCalls = append(am.esCalls, record)
+ //if len(am.esCalls) > am.maxRecords {
+ // am.esCalls = am.esCalls[1:]
+ //}
+}
+
+// RecordRedisCall 记录 Redis 调用
+func (am *APIMonitor) RecordRedisCall(operation, keyOrIndex, query string, duration time.Duration, err error) {
+ am.mutex.Lock()
+ defer am.mutex.Unlock()
+
+ record := APICallRecord{
+ ID: am.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: operation,
+ KeyOrIndex: keyOrIndex,
+ Query: query,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ am.redisCalls = append(am.redisCalls, record)
+ //if len(am.redisCalls) > am.maxRecords {
+ // am.redisCalls = am.redisCalls[1:]
+ //}
+}
+
+// getNextID 获取下一个 ID
+func (am *APIMonitor) getNextID() int64 {
+ id := am.nextID
+ am.nextID++
+ return id
+}
+
+// GetStats 获取统计数据
+func (am *APIMonitor) GetStats() *APIStats {
+ am.mutex.RLock()
+ defer am.mutex.RUnlock()
+
+ now := time.Now()
+ windowStart := now.Add(-am.qpsWindow)
+
+ // 统计 ES 调用
+ var esRecentCalls, esSuccessCalls int64
+ var esTotalDuration int64
+ for _, call := range am.esCalls {
+ if call.Timestamp.After(windowStart) {
+ esRecentCalls++
+ }
+ esTotalDuration += call.Duration
+ if call.Success {
+ esSuccessCalls++
+ }
+ }
+
+ // 统计 Redis 调用
+ var redisRecentCalls, redisSuccessCalls int64
+ var redisTotalDuration int64
+ for _, call := range am.redisCalls {
+ if call.Timestamp.After(windowStart) {
+ redisRecentCalls++
+ }
+ redisTotalDuration += call.Duration
+ if call.Success {
+ redisSuccessCalls++
+ }
+ }
+
+ totalCalls := int64(len(am.esCalls)) + int64(len(am.redisCalls))
+ successCalls := esSuccessCalls + redisSuccessCalls
+ totalDuration := esTotalDuration + redisTotalDuration
+
+ // 计算 QPS(每秒调用数)
+ windowSeconds := am.qpsWindow.Seconds()
+ esQPS := float64(esRecentCalls) / windowSeconds
+ redisQPS := float64(redisRecentCalls) / windowSeconds
+
+ var avgDuration int64
+ if totalCalls > 0 {
+ avgDuration = totalDuration / totalCalls
+ }
+
+ var successRate float64
+ if totalCalls > 0 {
+ successRate = float64(successCalls) / float64(totalCalls) * 100
+ }
+
+ return &APIStats{
+ Endpoint: am.endpoint,
+ TotalCalls: totalCalls,
+ Escalls: int64(len(am.esCalls)),
+ RedisCalls: int64(len(am.redisCalls)),
+ ESQPS: esQPS,
+ RedisQPS: redisQPS,
+ AvgDuration: avgDuration,
+ SuccessRate: successRate,
+ LastUpdate: now.Format("2006-01-02 15:04:05"),
+ }
+}
+
+// GetRecentESCalls 获取最近的 ES 调用记录(支持分页)
+func (am *APIMonitor) GetRecentESCalls(page, pageSize int) ([]APICallRecord, int) {
+ am.mutex.RLock()
+ defer am.mutex.RUnlock()
+
+ total := len(am.esCalls)
+ if total == 0 {
+ return []APICallRecord{}, 0
+ }
+
+ if page <= 0 {
+ page = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 50
+ }
+ if pageSize > 500 {
+ pageSize = 500
+ }
+
+ startIndex := (page - 1) * pageSize
+ endIndex := startIndex + pageSize
+
+ if startIndex >= total {
+ return []APICallRecord{}, total
+ }
+ if endIndex > total {
+ endIndex = total
+ }
+
+ result := make([]APICallRecord, endIndex-startIndex)
+ copy(result, am.esCalls[startIndex:endIndex])
+
+ for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+ result[i], result[j] = result[j], result[i]
+ }
+
+ return result, total
+}
+
+// GetRecentRedisCalls 获取最近的 Redis 调用记录(支持分页)
+func (am *APIMonitor) GetRecentRedisCalls(page, pageSize int) ([]APICallRecord, int) {
+ am.mutex.RLock()
+ defer am.mutex.RUnlock()
+
+ total := len(am.redisCalls)
+ if total == 0 {
+ return []APICallRecord{}, 0
+ }
+
+ if page <= 0 {
+ page = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 50
+ }
+ if pageSize > 500 {
+ pageSize = 500
+ }
+
+ startIndex := (page - 1) * pageSize
+ endIndex := startIndex + pageSize
+
+ if startIndex >= total {
+ return []APICallRecord{}, total
+ }
+ if endIndex > total {
+ endIndex = total
+ }
+
+ result := make([]APICallRecord, endIndex-startIndex)
+ copy(result, am.redisCalls[startIndex:endIndex])
+
+ for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+ result[i], result[j] = result[j], result[i]
+ }
+
+ return result, total
+}
+
+// GetRedisCallByID 根据 ID 获取 Redis 调用记录
+func (am *APIMonitor) GetRedisCallByID(callID int64) *APICallRecord {
+ am.mutex.RLock()
+ defer am.mutex.RUnlock()
+
+ for i := len(am.redisCalls) - 1; i >= 0; i-- {
+ if am.redisCalls[i].ID == callID {
+ return &am.redisCalls[i]
+ }
+ }
+ return nil
+}
+
+/*----------------------------------------ES-----------------------------------*/
+// MonitoredESClient 带监控的 ES 客户端
+type MonitoredESClient struct {
+ client *elasticsearch.Client
+ monitor *APIMonitor
+ endpoint string
+}
+
+// NewMonitoredESClient 创建带监控的 ES 客户端
+func NewMonitoredESClient(client *elasticsearch.Client, endpoint string) *MonitoredESClient {
+ //monitor := GetOrCreateAPIMonitor(endpoint, 1000, 1*time.Minute)
+ monitor := GetOrCreateAPIMonitor(endpoint, 1*time.Minute)
+ return &MonitoredESClient{
+ client: client,
+ monitor: monitor,
+ endpoint: endpoint,
+ }
+}
+
+// Search 带监控的 ES 搜索(使用 esapi.SearchRequest)
+func (m *MonitoredESClient) Search(ctx context.Context, req *esapi.SearchRequest) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ indexName := ""
+ if len(req.Index) > 0 {
+ indexName = strings.Join(req.Index, ",")
+ }
+
+ queryBody := ""
+ if req.Body != nil {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(req.Body)
+ queryBody = buf.String()
+ // 重新设置 Body,因为 ReadFrom 已经读取了
+ req.Body = io.NopCloser(buf)
+ }
+
+ req.Pretty = true
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "search",
+ KeyOrIndex: indexName,
+ Query: queryBody,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ //m.monitor.RecordAPICall(record, "es")
+ m.monitor.RecordESCall("search", indexName, queryBody, duration, err)
+
+ return resp, duration, err
+}
+
+// RecordAPICall 通用记录方法
+func (am *APIMonitor) RecordAPICall(record APICallRecord, callType string) {
+ am.mutex.Lock()
+ defer am.mutex.Unlock()
+
+ if callType == "es" {
+ am.esCalls = append(am.esCalls, record)
+ //if len(am.esCalls) > am.maxRecords {
+ // am.esCalls = am.esCalls[1:]
+ //}
+ } else if callType == "redis" {
+ am.redisCalls = append(am.redisCalls, record)
+ //if len(am.redisCalls) > am.maxRecords {
+ // am.redisCalls = am.redisCalls[1:]
+ //}
+ }
+}
+
+// SearchWithQuery 带监控的 ES 搜索(使用查询字符串)
+func (m *MonitoredESClient) SearchWithQuery(ctx context.Context, index, query string) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ req := esapi.SearchRequest{
+ Index: []string{index},
+ Body: strings.NewReader(query),
+ Pretty: true,
+ }
+
+ req.Pretty = true
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ m.monitor.RecordESCall("search", index, query, duration, err)
+
+ return resp, duration, err
+}
+
+// Get 带监控的 ES Get 文档
+func (m *MonitoredESClient) Get(ctx context.Context, index, docID string) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ req := esapi.GetRequest{
+ Index: index,
+ DocumentID: docID,
+ }
+
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ m.monitor.RecordESCall("get", index, "docID: "+docID, duration, err)
+
+ return resp, duration, err
+}
+
+// Index 带监控的 ES Index 文档
+func (m *MonitoredESClient) Index(ctx context.Context, index, docID string, body io.Reader) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ req := esapi.IndexRequest{
+ Index: index,
+ DocumentID: docID,
+ Body: body,
+ }
+
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ bodyStr := ""
+ if body != nil {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(body)
+ bodyStr = buf.String()
+ }
+
+ m.monitor.RecordESCall("index", index, bodyStr, duration, err)
+
+ return resp, duration, err
+}
+
+// Delete 带监控的 ES Delete 文档
+func (m *MonitoredESClient) Delete(ctx context.Context, index, docID string) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ req := esapi.DeleteRequest{
+ Index: index,
+ DocumentID: docID,
+ }
+
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ m.monitor.RecordESCall("delete", index, "docID: "+docID, duration, err)
+
+ return resp, duration, err
+}
+
+// Update 带监控的 ES Update 文档
+func (m *MonitoredESClient) Update(ctx context.Context, index, docID string, body io.Reader) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ req := esapi.UpdateRequest{
+ Index: index,
+ DocumentID: docID,
+ Body: body,
+ }
+
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ bodyStr := ""
+ if body != nil {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(body)
+ bodyStr = buf.String()
+ }
+
+ m.monitor.RecordESCall("update", index, bodyStr, duration, err)
+
+ return resp, duration, err
+}
+
+// Count 带监控的 ES Count
+func (m *MonitoredESClient) Count(ctx context.Context, index string, query string) (*esapi.Response, time.Duration, error) {
+ startTime := time.Now()
+
+ req := esapi.CountRequest{
+ Index: []string{index},
+ }
+
+ if query != "" {
+ req.Body = strings.NewReader(query)
+ }
+
+ resp, err := req.Do(ctx, m.client)
+ duration := time.Since(startTime)
+
+ m.monitor.RecordESCall("count", index, query, duration, err)
+
+ return resp, duration, err
+}
+
+// GetESCallByID 根据 ID 获取 ES 调用记录
+func (am *APIMonitor) GetESCallByID(callID int64) *APICallRecord {
+ am.mutex.RLock()
+ defer am.mutex.RUnlock()
+
+ for i := len(am.esCalls) - 1; i >= 0; i-- {
+ if am.esCalls[i].ID == callID {
+ return &am.esCalls[i]
+ }
+ }
+ return nil
+}
+
+/*-----------------------------------REDIS-----------------------------------*/
+// MonitoredRedisClient 带监控的 Redis 客户端
+type MonitoredRedisClient struct {
+ client *redis.Client
+ monitor *APIMonitor
+ endpoint string
+}
+
+// NewMonitoredRedisClient 创建带监控的 Redis 客户端
+func NewMonitoredRedisClient(client *redis.Client, endpoint string) *MonitoredRedisClient {
+ //monitor := GetOrCreateAPIMonitor(endpoint, 1000, 1*time.Minute)
+ monitor := GetOrCreateAPIMonitor(endpoint, 1*time.Minute)
+ return &MonitoredRedisClient{
+ client: client,
+ monitor: monitor,
+ endpoint: endpoint,
+ }
+}
+
+// Get 带监控的 Redis Get 操作
+func (m *MonitoredRedisClient) Get(ctx context.Context, key string) (string, time.Duration, error) {
+ startTime := time.Now()
+ val, err := m.client.Get(ctx, key).Result()
+ duration := time.Since(startTime)
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "GET",
+ KeyOrIndex: key,
+ Query: "",
+ Response: val,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+ //m.monitor.RecordAPICall(record, "redis")
+ m.monitor.RecordRedisCall("GET", key, "", duration, err)
+ return val, duration, err
+}
+
+// Set 带监控的 Redis Set 操作
+func (m *MonitoredRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) (string, time.Duration, error) {
+ startTime := time.Now()
+ err := m.client.Set(ctx, key, value, expiration).Err()
+ duration := time.Since(startTime)
+
+ query := ""
+ if str, ok := value.(string); ok {
+ query = str
+ } else if data, err := json.Marshal(value); err == nil {
+ query = string(data)
+ }
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "SET",
+ KeyOrIndex: key,
+ Query: query,
+ Response: "OK",
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ //m.monitor.RecordAPICall(record, "redis")
+ m.monitor.RecordRedisCall("SET", key, query, duration, err)
+
+ return "OK", duration, err
+}
+
+// ... existing code ...
+
+// HGet 带监控的 Redis HGet 操作
+func (m *MonitoredRedisClient) HGet(ctx context.Context, key, field string) (string, time.Duration, error) {
+ startTime := time.Now()
+ val, err := m.client.HGet(ctx, key, field).Result()
+ duration := time.Since(startTime)
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "HGET",
+ KeyOrIndex: key,
+ Query: "field: " + field,
+ Response: val,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ m.monitor.RecordAPICall(record, "redis")
+
+ return val, duration, err
+}
+
+// ... existing code ...
+
+// HSet 带监控的 Redis HSet 操作
+func (m *MonitoredRedisClient) HSet(ctx context.Context, key, field string, value interface{}) (int64, time.Duration, error) {
+ startTime := time.Now()
+ result, err := m.client.HSet(ctx, key, field, value).Result()
+ duration := time.Since(startTime)
+
+ queryValue := ""
+ if str, ok := value.(string); ok {
+ queryValue = str
+ } else if data, err := json.Marshal(value); err == nil {
+ queryValue = string(data)
+ }
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "HSET",
+ KeyOrIndex: key,
+ Query: fmt.Sprintf("field: %s, value: %v", field, queryValue),
+ Response: fmt.Sprintf("%d", result),
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ m.monitor.RecordAPICall(record, "redis")
+
+ return result, duration, err
+}
+
+// ... existing code ...
+
+// Exists 带监控的 Redis Exists 操作
+func (m *MonitoredRedisClient) Exists(ctx context.Context, keys ...string) (int64, time.Duration, error) {
+ startTime := time.Now()
+ result, err := m.client.Exists(ctx, keys...).Result()
+ duration := time.Since(startTime)
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "EXISTS",
+ KeyOrIndex: strings.Join(keys, ", "),
+ Query: "",
+ Response: fmt.Sprintf("%d", result),
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ m.monitor.RecordAPICall(record, "redis")
+
+ return result, duration, err
+}
+
+// ... existing code ...
+
+// Del 带监控的 Redis Del 操作
+func (m *MonitoredRedisClient) Del(ctx context.Context, keys ...string) (int64, time.Duration, error) {
+ startTime := time.Now()
+ result, err := m.client.Del(ctx, keys...).Result()
+ duration := time.Since(startTime)
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "DEL",
+ KeyOrIndex: strings.Join(keys, ", "),
+ Query: "",
+ Response: fmt.Sprintf("%d", result),
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ m.monitor.RecordAPICall(record, "redis")
+
+ return result, duration, err
+}
+
+// ... existing code ...
+
+// MGet 带监控的 Redis MGet 操作
+func (m *MonitoredRedisClient) MGet(ctx context.Context, keys ...string) ([]interface{}, time.Duration, error) {
+ startTime := time.Now()
+ result, err := m.client.MGet(ctx, keys...).Result()
+ duration := time.Since(startTime)
+
+ resultJSON := ""
+ if data, marshalErr := json.Marshal(result); marshalErr == nil {
+ resultJSON = string(data)
+ }
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "MGET",
+ KeyOrIndex: strings.Join(keys, ", "),
+ Query: "",
+ Response: resultJSON,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ m.monitor.RecordAPICall(record, "redis")
+
+ return result, duration, err
+}
+
+// ... existing code ...
+
+// Ping 带监控的 Redis Ping 操作
+func (m *MonitoredRedisClient) Ping(ctx context.Context) (string, time.Duration, error) {
+ startTime := time.Now()
+ val, err := m.client.Ping(ctx).Result()
+ duration := time.Since(startTime)
+
+ record := APICallRecord{
+ ID: m.monitor.getNextID(),
+ Timestamp: time.Now(),
+ Duration: duration.Milliseconds(),
+ Success: err == nil,
+ Operation: "PING",
+ KeyOrIndex: "",
+ Query: "PING",
+ Response: val,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ m.monitor.RecordAPICall(record, "redis")
+
+ return val, duration, err
+}
diff --git a/monitor/health_api.go b/monitor/health_api.go
new file mode 100644
index 0000000..ab7af1c
--- /dev/null
+++ b/monitor/health_api.go
@@ -0,0 +1,773 @@
+package monitor
+
+import (
+ "net/http"
+ "sort"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// APIMonitorController API 监控控制器
+type APIMonitorController struct{}
+
+// NewAPIMonitorController 创建 API 监控控制器
+func NewAPIMonitorController() *APIMonitorController {
+ return &APIMonitorController{}
+}
+
+// GetAPIStats 获取指定 API 的统计信息
+func (amc *APIMonitorController) GetAPIStats(c *gin.Context) {
+ endpoint := c.Query("endpoint")
+ if endpoint == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "缺少 endpoint 参数",
+ })
+ return
+ }
+
+ apiMonitorsMu.RLock()
+ monitor, exists := apiMonitors[endpoint]
+ apiMonitorsMu.RUnlock()
+
+ if !exists {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "该接口暂无监控数据",
+ })
+ return
+ }
+
+ stats := monitor.GetStats()
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": stats,
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetAllAPIStats 获取所有 API 的统计信息
+func (amc *APIMonitorController) GetAllAPIStats(c *gin.Context) {
+ apiMonitorsMu.RLock()
+ defer apiMonitorsMu.RUnlock()
+
+ allStats := make([]*APIStats, 0, len(apiMonitors))
+ endpoints := make([]string, 0, len(apiMonitors))
+
+ // 收集所有 endpoint
+ for endpoint := range apiMonitors {
+ endpoints = append(endpoints, endpoint)
+ }
+
+ // 按 endpoint 字母顺序排序
+ sort.Strings(endpoints)
+
+ // 按排序后的顺序获取统计信息
+ for _, endpoint := range endpoints {
+ allStats = append(allStats, apiMonitors[endpoint].GetStats())
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": allStats,
+ "count": len(allStats),
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetAPICallDetail 获取指定 API 调用的详细信息
+func (amc *APIMonitorController) GetAPICallDetail(c *gin.Context) {
+ endpoint := c.Query("endpoint")
+ callIDStr := c.Query("call_id")
+ callType := c.Query("type") // es or redis
+
+ if endpoint == "" || callIDStr == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "缺少 endpoint 或 call_id 参数",
+ })
+ return
+ }
+
+ callID, err := strconv.ParseInt(callIDStr, 10, 64)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "无效的 call_id",
+ })
+ return
+ }
+
+ apiMonitorsMu.RLock()
+ monitor, exists := apiMonitors[endpoint]
+ apiMonitorsMu.RUnlock()
+
+ if !exists {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "该接口暂无监控数据",
+ })
+ return
+ }
+
+ // 查找对应的调用记录
+ var foundRecord *APICallRecord
+ var recordType string
+
+ if callType == "es" {
+ foundRecord = monitor.GetESCallByID(callID)
+ if foundRecord != nil {
+ recordType = "ES"
+ }
+ } else if callType == "redis" {
+ foundRecord = monitor.GetRedisCallByID(callID)
+ if foundRecord != nil {
+ recordType = "Redis"
+ }
+ } else {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "type 参数必须是 es 或 redis",
+ })
+ return
+ }
+
+ if foundRecord == nil {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "未找到对应的调用记录",
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": gin.H{
+ "id": foundRecord.ID,
+ "type": recordType,
+ "timestamp": foundRecord.Timestamp.Format("2006-01-02 15:04:05"),
+ "duration_ms": foundRecord.Duration,
+ "success": foundRecord.Success,
+ "error": foundRecord.Error,
+ "operation": foundRecord.Operation,
+ "key_or_index": foundRecord.KeyOrIndex,
+ "query": foundRecord.Query,
+ "request": foundRecord.Request,
+ "response": foundRecord.Response,
+ },
+ })
+}
+
+// GetAPIESCalls 获取指定 API 的 ES 调用记录
+func (amc *APIMonitorController) GetAPIESCalls(c *gin.Context) {
+ endpoint := c.Query("endpoint")
+ if endpoint == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "缺少 endpoint 参数",
+ })
+ return
+ }
+
+ pageStr := c.DefaultQuery("page", "1")
+ pageSizeStr := c.DefaultQuery("page_size", "50")
+
+ page, err := strconv.Atoi(pageStr)
+ if err != nil || page <= 0 {
+ page = 1
+ }
+
+ pageSize, err := strconv.Atoi(pageSizeStr)
+ if err != nil || pageSize <= 0 {
+ pageSize = 50
+ }
+ if pageSize > 500 {
+ pageSize = 500
+ }
+
+ apiMonitorsMu.RLock()
+ monitor, exists := apiMonitors[endpoint]
+ apiMonitorsMu.RUnlock()
+
+ if !exists {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "该接口暂无监控数据",
+ })
+ return
+ }
+
+ calls, total := monitor.GetRecentESCalls(page, pageSize)
+ totalPages := (total + pageSize - 1) / pageSize
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": gin.H{
+ "calls": calls,
+ "count": len(calls),
+ "total": total,
+ "page": page,
+ "page_size": pageSize,
+ "total_pages": totalPages,
+ },
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetAPIRedisCalls 获取指定 API 的 Redis 调用记录
+func (amc *APIMonitorController) GetAPIRedisCalls(c *gin.Context) {
+ endpoint := c.Query("endpoint")
+ if endpoint == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "缺少 endpoint 参数",
+ })
+ return
+ }
+
+ pageStr := c.DefaultQuery("page", "1")
+ pageSizeStr := c.DefaultQuery("page_size", "50")
+
+ page, err := strconv.Atoi(pageStr)
+ if err != nil || page <= 0 {
+ page = 1
+ }
+
+ pageSize, err := strconv.Atoi(pageSizeStr)
+ if err != nil || pageSize <= 0 {
+ pageSize = 50
+ }
+ if pageSize > 500 {
+ pageSize = 500
+ }
+
+ apiMonitorsMu.RLock()
+ monitor, exists := apiMonitors[endpoint]
+ apiMonitorsMu.RUnlock()
+
+ if !exists {
+ c.JSON(http.StatusNotFound, gin.H{
+ "error": "该接口暂无监控数据",
+ })
+ return
+ }
+
+ calls, total := monitor.GetRecentRedisCalls(page, pageSize)
+ totalPages := (total + pageSize - 1) / pageSize
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": gin.H{
+ "calls": calls,
+ "count": len(calls),
+ "total": total,
+ "page": page,
+ "page_size": pageSize,
+ "total_pages": totalPages,
+ },
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetAPIMonitorDashboard 获取 API 监控仪表板
+func (amc *APIMonitorController) GetAPIMonitorDashboard(c *gin.Context) {
+ dashboardHTML := `
+
+
+
+
+
API 监控仪表板
+
+
+
+
+
🔍 API 监控仪表板 - ES & Redis 调用
+
+
+
+
+
+
+
+
+
+
详细监控数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`
+
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, dashboardHTML)
+}
diff --git a/service/book.go b/service/book.go
index ba1acbe..91e2d16 100644
--- a/service/book.go
+++ b/service/book.go
@@ -262,46 +262,12 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
if book == nil {
return nil, fmt.Errorf("未找到该 ISBN 对应的图书")
}
+ // 获取字段配置
+ fieldConfig := es.GetESFieldConfig()
// 构建更新脚本
var scriptParts []string
params := make(map[string]interface{})
- allowedFields := map[string]bool{
- "book_name": true,
- "book_pic": true,
- "book_pic_s": true,
- "book_pic_b": true,
- "book_pic_w": true,
- "author": true,
- "category": true,
- "publisher": true,
- "publication_time": true,
- "binding_layout": true,
- "fix_price": true,
- "content": true,
- "is_suit": true,
- "day_sale_7": true,
- "day_sale_15": true,
- "day_sale_30": true,
- "day_sale_60": true,
- "day_sale_90": true,
- "day_sale_180": true,
- "day_sale_365": true,
- "this_year_sale": true,
- "last_year_sale": true,
- "total_sale": true,
- "buy_counts": true,
- "sell_counts": true,
- "is_illegal": true,
- "is_return": true,
- "is_filter": true,
- "update_time": true,
- "page_count": true,
- "word_count": true,
- "book_format": true,
- "cat_id": true,
- }
-
// 判断 is_suit 是否已传递,如果没传则自动检测
if _, exists := request.Data["is_suit"]; !exists {
// 未传递 is_suit,自动检测并设置
@@ -310,7 +276,9 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
}
for field, value := range request.Data {
- if !allowedFields[field] {
+ // 使用配置检查字段是否允许更新
+ if !fieldConfig.IsAllowUpdate(field) {
+ log.Printf("[UpdateBookFieldsByISBN] 字段 %s 不允许更新,已跳过", field)
continue
}
scriptParts = append(scriptParts,
@@ -320,7 +288,6 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
if len(scriptParts) == 0 {
return nil, fmt.Errorf("没有有效的更新字段")
}
-
body := map[string]interface{}{
"script": map[string]interface{}{
"source": strings.Join(scriptParts, " "),
@@ -588,9 +555,9 @@ func (svc *BookService) buildUpdateData(existing, new *es.ESBook) map[string]int
{existing.PublicationTime == "" && new.PublicationTime != "", "publication_time", new.PublicationTime},
{existing.BookName.Value == "" && new.BookName.Value != "", "book_name", new.BookName.Value},
{existing.Author == "" && new.Author != "", "author", new.Author},
- {existing.BookPic.PddPath == "" && new.BookPic.PddPath != "", "book_pic",
+ {existing.BookPic.PddPath == "" && new.BookPic.PddPath != "" && strings.Contains(new.BookPic.PddPath, "http"), "book_pic",
map[string]interface{}{"localPath": "", "pddPath": new.BookPic.PddPath}},
- {existing.BookPicS.PddResponse == "" && new.BookPicS.PddResponse != "", "book_pic_s",
+ {existing.BookPicS.PddResponse == "" && new.BookPicS.PddResponse != "" && strings.Contains(new.BookPicS.PddResponse, "http"), "book_pic_s",
map[string]interface{}{"localPath": "", "pddResponse": new.BookPicS.PddResponse}},
}
@@ -620,55 +587,66 @@ func (svc *BookService) addBookToES(ctx context.Context, req *es.ESBook) (*es.ES
if req.PublicationTime != "" && req.PublicationTime != "0" {
publicationTimeTimestamp = strconv.FormatInt(util.ParsePublicationTime(publicationTimeTimestamp), 10)
}
+ // 准备数据映射(使用 Go 字段名)
+ dataMap := make(map[string]interface{})
// 构建 ES 文档
- doc := map[string]interface{}{
- "id": svc.generateNewID(),
- "book_name": req.BookName.Value,
- "book_pic": es.BookPicObj{LocalPath: "", PddPath: req.BookPic.PddPath},
- "book_pic_s": es.BookPicSObj{LocalPath: "", PddResponse: req.BookPicS.PddResponse},
- "book_pic_b": req.BookPicB,
- "book_pic_w": make(map[string]interface{}),
- "isbn": req.ISBN,
- "author": req.Author,
- "category": req.Category,
- "publisher": req.Publisher,
- "publication_time": publicationTimeTimestamp,
- "binding_layout": req.BindingLayout,
- "fix_price": req.FixPrice,
- "content": req.Content,
- "is_suit": map[bool]int{true: 1, false: 0}[es.CheckBookSuit(req.BookName.Value)],
- "day_sale_7": salesInfo.DaySale7,
- "day_sale_15": salesInfo.DaySale15,
- "day_sale_30": salesInfo.DaySale30,
- "day_sale_60": salesInfo.DaySale60,
- "day_sale_90": salesInfo.DaySale90,
- "day_sale_180": salesInfo.DaySale180,
- "day_sale_365": salesInfo.DaySale365,
- "this_year_sale": salesInfo.ThisYearSale,
- "last_year_sale": salesInfo.LastYearSale,
- "total_sale": salesInfo.TotalSale,
- "buy_counts": req.BuyCounts,
- "sell_counts": req.SellCounts,
- "book_pic_obj": req.BookPicObj,
- "book_pic_obj_s": req.BookPicObjS,
- "is_illegal": 0,
- "is_return": 0,
- "is_filter": "000000",
- "update_time": es.NumberOrString(fmt.Sprintf("%d", time.Now().Unix())),
- "page_count": req.PageCount,
- "word_count": req.WordCount,
- "book_format": req.BookFormat,
+ dataMap["ID"] = svc.generateNewID()
+ dataMap["BookName"] = req.BookName.Value
+ dataMap["BookPic"] = es.BookPicObj{LocalPath: "", PddPath: req.BookPic.PddPath}
+ dataMap["BookPicS"] = es.BookPicSObj{LocalPath: "", PddResponse: req.BookPicS.PddResponse}
+ dataMap["BookPicB"] = req.BookPicB
+ dataMap["BookPicW"] = make(map[string]interface{})
+ dataMap["ISBN"] = req.ISBN
+ dataMap["Author"] = req.Author
+ dataMap["Category"] = req.Category
+ dataMap["Publisher"] = req.Publisher
+ dataMap["PublicationTime"] = publicationTimeTimestamp
+ dataMap["BindingLayout"] = req.BindingLayout
+ dataMap["FixPrice"] = req.FixPrice
+ dataMap["Content"] = req.Content
+ dataMap["IsSuit"] = map[bool]int{true: 1, false: 0}[es.CheckBookSuit(req.BookName.Value)]
+ // 销量字段
+ dataMap["DaySale7"] = salesInfo.DaySale7
+ dataMap["DaySale15"] = salesInfo.DaySale15
+ dataMap["DaySale30"] = salesInfo.DaySale30
+ dataMap["DaySale60"] = salesInfo.DaySale60
+ dataMap["DaySale90"] = salesInfo.DaySale90
+ dataMap["DaySale180"] = salesInfo.DaySale180
+ dataMap["DaySale365"] = salesInfo.DaySale365
+ dataMap["ThisYearSale"] = salesInfo.ThisYearSale
+ dataMap["LastYearSale"] = salesInfo.LastYearSale
+ dataMap["TotalSale"] = salesInfo.TotalSale
+ dataMap["BuyCounts"] = req.BuyCounts
+ dataMap["SellCounts"] = req.SellCounts
+ dataMap["BookPicObj"] = req.BookPicObj
+ dataMap["BookPicObjS"] = req.BookPicObjS
+ dataMap["IsIllegal"] = 0
+ dataMap["IsReturn"] = 0
+ dataMap["IsFilter"] = "000000"
+
+ // 时间字段
+ dataMap["UpdateTime"] = es.NumberOrString(fmt.Sprintf("%d", time.Now().Unix()))
+
+ // 其他字段
+ dataMap["PageCount"] = req.PageCount
+ dataMap["WordCount"] = req.WordCount
+ dataMap["BookFormat"] = req.BookFormat
+ if req.Other != nil {
+ dataMap["Other"] = req.Other
}
- // 写入 ES
+ fmt.Println("dataMap:", dataMap)
+
+ // 使用配置构建 ES 文档(自动转换字段名)
+ doc := es.GetESFieldConfig().BuildESDocument(dataMap)
+ //// 写入 ES
if err := svc.indexDocumentToES(ctx, doc, req.ISBN); err != nil {
return nil, err
}
// 同步 Redis
_ = svc.SyncRedisByISBN(req.ISBN, "update")
-
- // 构建返回对象
- return &es.ESBook{
+ // 建返回对象
+ returnBook := &es.ESBook{
ID: doc["id"].(int64),
BookName: es.FlexibleString{Value: req.BookName.Value},
BookPic: doc["book_pic"].(es.BookPicObj),
@@ -705,7 +683,9 @@ func (svc *BookService) addBookToES(ctx context.Context, req *es.ESBook) (*es.ES
PageCount: req.PageCount,
WordCount: req.WordCount,
BookFormat: req.BookFormat,
- }, nil
+ Other: req.Other,
+ }
+ return returnBook, nil
}
// SyncRedisByISBN 同步到Redis