Compare commits

...

10 Commits

Author SHA1 Message Date
97694731
bd6635f5be 套装书修改逻辑 2026-06-15 13:29:51 +08:00
97694731
701e7c75b1 ES公网IP-->内网IP 2026-05-08 16:51:25 +08:00
97694731
d52c24edf5 类目更新接口修改,三个平台同时更新-->只修改传入的平台的类目 2026-05-08 16:49:19 +08:00
unknown
ce1261dcbf 孔夫子分类筛选 2026-04-30 18:08:43 +08:00
unknown
6894d4892d Merge branch 'master' of https://github.com/97694731/centerBook 2026-04-18 17:20:54 +08:00
unknown
ec6e27e023 psi api 2026-04-18 17:20:38 +08:00
97694731
1db0342973 es地址切换 2026-04-18 17:16:06 +08:00
unknown
7f14b6cf53 优化选品中心 2026-04-03 16:04:59 +08:00
97694731
f1503b6490 代码同步 2026-03-31 10:01:43 +08:00
unknown
d1e2366a4d 字段兼容 2026-03-28 19:15:27 +08:00
6 changed files with 2261 additions and 782 deletions

View File

@ -31,25 +31,25 @@ func GetESFieldConfig() *ESFieldConfig {
"fix_price": true, "fix_price": true,
"content": true, "content": true,
"is_suit": true, "is_suit": true,
"day_sale_7": true, //"day_sale_7": true,
"day_sale_15": true, //"day_sale_15": true,
"day_sale_30": true, //"day_sale_30": true,
"day_sale_60": true, //"day_sale_60": true,
"day_sale_90": true, //"day_sale_90": true,
"day_sale_180": true, //"day_sale_180": true,
"day_sale_365": true, //"day_sale_365": true,
"this_year_sale": true, //"this_year_sale": true,
"last_year_sale": true, //"last_year_sale": true,
"total_sale": true, //"total_sale": true,
"buy_counts": true, //"buy_counts": true,
"sell_counts": true, //"sell_counts": true,
"is_illegal": true, "is_illegal": true,
"is_return": true, "is_return": true,
"is_filter": true, "is_filter": true,
"update_time": true, "update_time": true,
"page_count": true, //"page_count": true,
"word_count": true, //"word_count": true,
"book_format": true, //"book_format": true,
"cat_id": true, "cat_id": true,
"other": true, // 允许更新 other 字段 "other": true, // 允许更新 other 字段
}, },

View File

@ -26,6 +26,7 @@ import (
) )
const ESIndex = "books-from-mysql-v2" const ESIndex = "books-from-mysql-v2"
const ESIndexV3 = "books-from-mysql-v3"
// ESBookResponse 用于返回给Java客户端的格式ID为简单的int64 // ESBookResponse 用于返回给Java客户端的格式ID为简单的int64
type ESBookResponse struct { type ESBookResponse struct {
@ -70,6 +71,49 @@ type ESBookResponse struct {
Other map[string]interface{} `json:"other"` // 扩展字段,用于兼容未来新增字段 Other map[string]interface{} `json:"other"` // 扩展字段,用于兼容未来新增字段
} }
// ESBookResponseByPSI 用于返回给Java客户端的格式ID为简单的int64
type ESBookResponseByPSI struct {
ID int64 `json:"id"`
BookName string `json:"book_name"`
BookPic map[string]interface{} `json:"book_pic"`
BookPicS map[string]interface{} `json:"book_pic_s"`
BookPicB string `json:"book_pic_b"`
BookPicW map[string]interface{} `json:"book_pic_w"`
BookDefPic map[string]interface{} `json:"book_def_pic"` // 自制官图,临时用
ISBN string `json:"isbn"`
Author string `json:"author"`
Category string `json:"category"`
Publisher string `json:"publisher"`
PublicationTime string `json:"publication_time"`
BindingLayout string `json:"binding_layout"`
FixPrice float64 `json:"fix_price"`
Content string `json:"content"`
IsSuit int `json:"is_suit"`
DaySale7 int `json:"day_sale_7"`
DaySale15 int `json:"day_sale_15"`
DaySale30 int `json:"day_sale_30"`
DaySale60 int `json:"day_sale_60"`
DaySale90 int `json:"day_sale_90"`
DaySale180 int `json:"day_sale_180"`
DaySale365 int `json:"day_sale_365"`
ThisYearSale int `json:"this_year_sale"`
LastYearSale int `json:"last_year_sale"`
TotalSale int `json:"total_sale"`
BuyCounts int64 `json:"buy_counts"`
SellCounts int64 `json:"sell_counts"`
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"` // 过滤字段
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 处理可能是字符串或数组的字段 // FlexibleString 处理可能是字符串或数组的字段
type FlexibleString struct { type FlexibleString struct {
Value string Value string
@ -162,6 +206,10 @@ func (book *ESBook) ConvertToResponse() ESBookResponse {
publicationTime := book.PublicationTime publicationTime := book.PublicationTime
if publicationTime == "" || publicationTime == "0" || publicationTime == "0000-00-00" { if publicationTime == "" || publicationTime == "0" || publicationTime == "0000-00-00" {
publicationTime = time.Unix(0, 0).Format("2006-01") publicationTime = time.Unix(0, 0).Format("2006-01")
} else {
if timeVal, err := strconv.ParseInt(publicationTime, 10, 64); err == nil {
publicationTime = strconv.FormatInt(timeVal-5364000000, 10)
}
} }
return ESBookResponse{ return ESBookResponse{
ID: book.ID, ID: book.ID,
@ -206,6 +254,71 @@ func (book *ESBook) ConvertToResponse() ESBookResponse {
} }
} }
// ConvertToResponseByPsi 将ESBook转换为ESBookResponse处理ID字段转换
func (book *ESBook) ConvertToResponseByPsi() ESBookResponseByPSI {
bookPicMap := map[string]interface{}{
"localPath": book.BookPic.LocalPath,
"pddPath": book.BookPic.PddPath,
}
bookPicSMap := map[string]interface{}{
"localPath": book.BookPicS.LocalPath,
"pddResponse": book.BookPicS.PddResponse,
}
bookDefPic := map[string]interface{}{
"localPath": book.BookDefPic.LocalPath,
"pddPath": book.BookDefPic.PddPath,
}
publicationTime := book.PublicationTime
if publicationTime == "" || publicationTime == "0" || publicationTime == "0000-00-00" {
publicationTime = time.Unix(0, 0).Format("2006-01")
} else {
if timeVal, err := strconv.ParseInt(publicationTime, 10, 64); err == nil {
publicationTime = strconv.FormatInt(timeVal-5364000000, 10)
}
}
return ESBookResponseByPSI{
ID: book.ID,
BookName: book.BookName.Value,
BookPic: bookPicMap,
BookPicS: bookPicSMap,
BookPicB: book.BookPicB,
BookPicW: book.BookPicW,
BookDefPic: bookDefPic,
ISBN: book.ISBN,
Author: book.Author,
Category: book.Category,
Publisher: book.Publisher,
PublicationTime: publicationTime,
BindingLayout: book.BindingLayout,
FixPrice: float64(book.FixPrice),
Content: book.Content,
IsSuit: book.IsSuit,
DaySale7: book.DaySale7,
DaySale15: book.DaySale15,
DaySale30: book.DaySale30,
DaySale60: book.DaySale60,
DaySale90: book.DaySale90,
DaySale180: book.DaySale180,
DaySale365: book.DaySale365,
ThisYearSale: book.ThisYearSale,
LastYearSale: book.LastYearSale,
TotalSale: book.TotalSale,
BuyCounts: book.BuyCounts,
SellCounts: book.SellCounts,
BookPicObj: book.BookPicObj,
BookPicObjS: book.BookPicObjS,
UpdateTime: book.UpdateTime,
IsIllegal: book.IsIllegal,
IsReturn: book.IsReturn,
IsFilter: book.IsFilter,
PageCount: book.PageCount,
WordCount: book.WordCount,
BookFormat: book.BookFormat,
CatId: book.CatId,
Other: book.Other,
}
}
// 用于 book_pic // 用于 book_pic
type BookPicObj struct { type BookPicObj struct {
LocalPath string `json:"localPath"` LocalPath string `json:"localPath"`
@ -243,18 +356,18 @@ type ESBook struct {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
IsSuit int `json:"is_suit,omitempty"` IsSuit int `json:"is_suit,omitempty"`
// ⭐ 新增的字段 // ⭐ 新增的字段
DaySale7 int `json:"day_sale_7,omitempty"` DaySale7 int `json:"day_sale_7"`
DaySale15 int `json:"day_sale_15,omitempty"` DaySale15 int `json:"day_sale_15"`
DaySale30 int `json:"day_sale_30,omitempty"` DaySale30 int `json:"day_sale_30"`
DaySale60 int `json:"day_sale_60,omitempty"` DaySale60 int `json:"day_sale_60"`
DaySale90 int `json:"day_sale_90,omitempty"` DaySale90 int `json:"day_sale_90"`
DaySale180 int `json:"day_sale_180,omitempty"` DaySale180 int `json:"day_sale_180"`
DaySale365 int `json:"day_sale_365,omitempty"` DaySale365 int `json:"day_sale_365"`
ThisYearSale int `json:"this_year_sale,omitempty"` ThisYearSale int `json:"this_year_sale"`
LastYearSale int `json:"last_year_sale,omitempty"` LastYearSale int `json:"last_year_sale"`
TotalSale int `json:"total_sale,omitempty"` TotalSale int `json:"total_sale"`
BuyCounts int64 `json:"buy_counts,omitempty"` BuyCounts int64 `json:"buy_counts"`
SellCounts int64 `json:"sell_counts,omitempty"` SellCounts int64 `json:"sell_counts"`
BookPicObj map[string]interface{} `json:"book_pic_obj,omitempty"` BookPicObj map[string]interface{} `json:"book_pic_obj,omitempty"`
BookPicObjS map[string]interface{} `json:"book_pic_obj_s,omitempty"` BookPicObjS map[string]interface{} `json:"book_pic_obj_s,omitempty"`
UpdateTime NumberOrString `json:"update_time,omitempty"` UpdateTime NumberOrString `json:"update_time,omitempty"`
@ -319,7 +432,9 @@ type AddBookFullRequest struct {
type ESSearchService struct { type ESSearchService struct {
ES *ESClient ES *ESClient
SyncRedisByISBN func(isbn string, act string) error
} }
type NumberOrString string type NumberOrString string
func (n *NumberOrString) UnmarshalJSON(data []byte) error { func (n *NumberOrString) UnmarshalJSON(data []byte) error {
@ -1487,11 +1602,6 @@ func (svc *ESSearchService) SearchBookBaseInfoES(c *gin.Context) ([]ESBook, int,
} }
defer res.Body.Close() defer res.Body.Close()
//if res.IsError() {
// raw, _ := io.ReadAll(res.Body)
// fmt.Printf("[ERROR] ES error body: %s\n", string(raw))
// return nil, 0, fmt.Errorf("ES error: %s", string(raw))
//}
// 读取响应 // 读取响应
var buf bytes.Buffer var buf bytes.Buffer
// 使用CopyBuffer可以重用缓冲区 // 使用CopyBuffer可以重用缓冲区
@ -1522,11 +1632,6 @@ func (svc *ESSearchService) SearchBookBaseInfoES(c *gin.Context) ([]ESBook, int,
if err := _json.Unmarshal(rawData, &parsed); err != nil { if err := _json.Unmarshal(rawData, &parsed); err != nil {
return nil, 0, fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, string(rawData[:min(200, len(rawData))])) return nil, 0, fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, string(rawData[:min(200, len(rawData))]))
} }
//var parsed esHitsWrapper
//if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
// fmt.Printf("[ERROR] JSON Decode error: %v\n", err)
// return nil, 0, err
//}
list := make([]ESBook, 0, len(parsed.Hits.Hits)) list := make([]ESBook, 0, len(parsed.Hits.Hits))
for _, hit := range parsed.Hits.Hits { for _, hit := range parsed.Hits.Hits {
list = append(list, hit.Source) list = append(list, hit.Source)
@ -1588,7 +1693,7 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
continue continue
} }
// 不作为查询条件的字段 // 不作为查询条件的字段
if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" || key == "shopType" { if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" || key == "shopType" || key == "kongfz_include" {
continue continue
} }
@ -1610,7 +1715,6 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
key = nk key = nk
} }
// ===== is_suit =====
if key == "is_suit" { if key == "is_suit" {
fmt.Printf("[DEBUG] is_suit val=%q\n", val) fmt.Printf("[DEBUG] is_suit val=%q\n", val)
if num, err := strconv.Atoi(val); err == nil { if num, err := strconv.Atoi(val); err == nil {
@ -1625,7 +1729,6 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
continue continue
} }
// ===== is_return =====
if key == "is_return" { if key == "is_return" {
fmt.Printf("[DEBUG] is_return val=%q\n", val) fmt.Printf("[DEBUG] is_return val=%q\n", val)
if num, err := strconv.Atoi(val); err == nil { if num, err := strconv.Atoi(val); err == nil {
@ -1640,7 +1743,6 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
continue continue
} }
// ===== is_filter =====
// ===== is_filter按 shopType 位匹配)===== // ===== is_filter按 shopType 位匹配)=====
if key == "is_filter" { if key == "is_filter" {
fmt.Printf("[DEBUG] is_filter val=%q\n", val) fmt.Printf("[DEBUG] is_filter val=%q\n", val)
@ -1812,6 +1914,7 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
}, },
}, },
} }
must = append(must, cond) must = append(must, cond)
fmt.Printf("[DEBUG] must += %v\n", cond) fmt.Printf("[DEBUG] must += %v\n", cond)
continue continue
@ -1837,6 +1940,7 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
if len(parts) == 2 { if len(parts) == 2 {
minVal, _ := strconv.Atoi(parts[0]) minVal, _ := strconv.Atoi(parts[0])
maxVal, _ := strconv.Atoi(parts[1]) maxVal, _ := strconv.Atoi(parts[1])
cond := map[string]interface{}{ cond := map[string]interface{}{
"range": map[string]interface{}{ "range": map[string]interface{}{
"total_sale": map[string]interface{}{ "total_sale": map[string]interface{}{
@ -1851,6 +1955,112 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
} }
} }
// ===== page_count 范围查询 =====
if key == "page_count" {
parts := strings.Split(val, ",")
if len(parts) == 2 {
minVal, _ := strconv.Atoi(parts[0])
maxVal, _ := strconv.Atoi(parts[1])
cond := map[string]interface{}{
"range": map[string]interface{}{
"page_count": map[string]interface{}{
"gte": minVal,
"lte": maxVal,
},
},
}
must = append(must, cond)
fmt.Printf("[DEBUG] must += %v (page_count: %d-%d)\n", cond, minVal, maxVal)
continue
}
}
// ===== word_count 范围查询 =====
if key == "word_count" {
parts := strings.Split(val, ",")
if len(parts) == 2 {
minVal, _ := strconv.Atoi(parts[0])
maxVal, _ := strconv.Atoi(parts[1])
cond := map[string]interface{}{
"range": map[string]interface{}{
"word_count": map[string]interface{}{
"gte": minVal,
"lte": maxVal,
},
},
}
must = append(must, cond)
fmt.Printf("[DEBUG] must += %v (word_count: %d-%d)\n", cond, minVal, maxVal)
continue
}
}
// ===== kongfz_categories孔夫子分类=====
if key == "kongfz_categories" {
kongfzInclude := c.DefaultQuery("kongfz_include", "1")
// 分割多个分类ID
categories := strings.Split(val, ",")
if len(categories) > 0 {
// 过滤空值
var validCategories []string
for _, catID := range categories {
catID = strings.TrimSpace(catID)
if catID != "" {
validCategories = append(validCategories, catID)
}
}
if len(validCategories) > 0 {
if kongfzInclude == "2" {
// 否查询不包含这些分类的数据must_not
var mustNotQueries []map[string]interface{}
for _, catID := range validCategories {
mustNotQueries = append(mustNotQueries, map[string]interface{}{
"prefix": map[string]interface{}{
"cat_id.kong_fu_zi_cat_id": catID,
},
})
}
if len(mustNotQueries) > 0 {
cond := map[string]interface{}{
"bool": map[string]interface{}{
"must_not": mustNotQueries,
},
}
must = append(must, cond)
fmt.Printf("[DEBUG] must += %v (kongfz exclude: %d categories)\n", cond, len(mustNotQueries))
}
} else {
// 是查询包含这些分类的数据should + minimum_should_match=1
var shouldQueries []map[string]interface{}
for _, catID := range validCategories {
shouldQueries = append(shouldQueries, map[string]interface{}{
"prefix": map[string]interface{}{
"cat_id.kong_fu_zi_cat_id": catID,
},
})
}
if len(shouldQueries) > 0 {
cond := map[string]interface{}{
"bool": map[string]interface{}{
"should": shouldQueries,
"minimum_should_match": 1,
},
}
must = append(must, cond)
fmt.Printf("[DEBUG] must += %v (kongfz include: %d categories)\n", cond, len(shouldQueries))
}
}
}
}
continue
}
// ===== 数值范围 ===== // ===== 数值范围 =====
if key == "sell_counts" || if key == "sell_counts" ||
strings.HasPrefix(key, "day_sale_") || strings.HasPrefix(key, "day_sale_") ||
@ -1870,6 +2080,7 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
}, },
}, },
} }
must = append(must, cond) must = append(must, cond)
fmt.Printf("[DEBUG] must += %v\n", cond) fmt.Printf("[DEBUG] must += %v\n", cond)
continue continue
@ -1944,21 +2155,6 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
body, _ := json.MarshalIndent(query, "", " ") body, _ := json.MarshalIndent(query, "", " ")
fmt.Printf("[DEBUG] ES Query Body:\n%s\n", string(body)) 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),
//)
//
//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() endpoint := c.FullPath()
monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint) monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint)
@ -1972,7 +2168,7 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in
res, duration, err := monitoredES.Search(context.Background(), &req) res, duration, err := monitoredES.Search(context.Background(), &req)
fmt.Printf("[DEBUG] ES Query Response:\n%s\n", res) //fmt.Printf("[DEBUG] ES Query Response:\n%s\n", res)
fmt.Printf("[DEBUG] ES 查询耗时:%dms\n", duration.Milliseconds()) fmt.Printf("[DEBUG] ES 查询耗时:%dms\n", duration.Milliseconds())
if err != nil { if err != nil {
@ -2294,165 +2490,191 @@ func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) {
}) })
} }
// func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) { func (svc *ESSearchService) SearchByISBNByXyCallBack(c *gin.Context) {
// isbn := c.Query("isbn")
// if isbn == "" { isbn := c.Query("isbn")
// c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) if isbn == "" {
// return log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数")
// } c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
// return
// ctx := context.Background() }
//
// db4Client, err := redisClient.GetClientByName("db1") log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn)
// if err == nil {
// val, err := db4Client.Get(ctx, isbn).Result() ctx := context.Background()
// if err == nil && val != "" { endpoint := c.FullPath()
// log.Printf("[SearchBooksHandler] 从 Redis db1 查询到数据: %s", isbn)
// // 使用 RedisBookInfo 结构体解析 // ES 查询(使用监控)
// var redisBook request.BookInfo query := map[string]interface{}{
// if err := json.Unmarshal([]byte(val), &redisBook); err == nil { "query": map[string]interface{}{
// // 转换为 ESBook "term": map[string]interface{}{
// esBook := ConvertRedisBookToESBook(&redisBook) "isbn": isbn,
// if esBook != nil { },
// responseData := esBook.ConvertToResponse() },
// c.JSON(200, gin.H{ "_source": true,
// "data": responseData, }
// })
// return body, err := json.Marshal(query)
// } if err != nil {
// } else { log.Printf("[SearchBookByISBNHandler] 构建查询 JSON 失败:%v", err)
// log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err) c.JSON(500, gin.H{"error": "构建查询失败"})
// } return
// } }
// }
// req := esapi.SearchRequest{
// result, err := svc.SearchBooks(isbn) Index: []string{ESIndex},
// if err != nil { Body: bytes.NewReader(body),
// c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()}) TrackTotalHits: true,
// return Pretty: true,
// } }
//
// responseList := make([]ESBookResponse, 0, len(result)) // 创建监控客户端并执行查询
// for _, book := range result { monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint)
// responseList = append(responseList, book.ConvertToResponse()) resp, duration, err := monitoredES.Search(ctx, &req)
// }
// log.Printf("[SearchBookByISBNHandler] ES 查询耗时:%dms", duration.Milliseconds())
// c.JSON(200, gin.H{
// "count": len(result), if err != nil {
// "data": responseList, log.Printf("[SearchBookByISBNHandler] ES 查询失败:%v", err)
// }) c.JSON(500, gin.H{"error": "ES 查询失败:" + err.Error()})
// } return
//func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) { }
// defer resp.Body.Close()
// isbn := c.Query("isbn")
// if isbn == "" { if resp.IsError() {
// log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数") log.Printf("[SearchBookByISBNHandler] ES 返回错误:%s", resp.String())
// c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) c.JSON(500, gin.H{"error": "ES 返回错误:" + resp.String()})
// return return
// } }
//
// log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn) var parsed esHitsWrapper
// if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
// ctx := context.Background() log.Printf("[SearchBookByISBNHandler] 解析 ES 响应失败:%v", err)
// c.JSON(500, gin.H{"error": "解析响应失败"})
// db4Client, err := redisClient.GetClientByName("db1") return
// fmt.Println(db4Client) }
//
// if err != nil { var result *ESBook
// log.Printf("[SearchBookByISBNHandler] 获取 Redis db1 客户端失败: %v", err) if len(parsed.Hits.Hits) > 0 {
// } else { result = &parsed.Hits.Hits[0].Source
// val, err := db4Client.Get(ctx, isbn).Result() }
// if err == nil && val != "" {
// log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据: %s", isbn) if result == nil {
// // 使用 RedisBookInfo 结构体解析 c.JSON(200, gin.H{
// var redisBook request.BookInfo "data": nil,
// if err := json.Unmarshal([]byte(val), &redisBook); err == nil { })
// // 转换为 ESBook return
// esBook := ConvertRedisBookToESBook(&redisBook) }
// if esBook != nil {
// responseData := esBook.ConvertToResponse() responseData := result.ConvertToResponse()
// c.JSON(200, gin.H{ c.JSON(200, gin.H{
// "data": responseData, "data": responseData,
// }) })
// return }
// }
// } else { func (svc *ESSearchService) SearchBookByISBNHandlerToPsi(c *gin.Context) {
// log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err) isbn := c.Query("isbn")
// } if isbn == "" {
// } else { log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数")
// log.Printf("[SearchBookByISBNHandler] Redis db1 中未找到 ISBN: %s", isbn) c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
// } return
// } }
//
// result, err := svc.SearchBookByISBN(isbn) log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn)
// if err != nil {
// log.Printf("[SearchBookByISBNHandler] ES 查询失败: %v", err) ctx := context.Background()
// c.JSON(500, gin.H{"error": err.Error()}) endpoint := c.FullPath()
// return
// } // Redis 查询(使用监控)
// db1Client, err := redisClient.GetClientByName("db10")
// if result == nil { if err == nil {
// log.Printf("[SearchBookByISBNHandler] ES 中未找到 ISBN: %s从孔夫子抓取", isbn) monitoredRedis := monitor.NewMonitoredRedisClient(db1Client, endpoint)
// val, _, err := monitoredRedis.Get(ctx, isbn)
// apiBook, err := kongfz.GetBookImageByISBN(isbn, "CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f") if err == nil && val != "" {
// if err != nil { log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据:%s", isbn)
// log.Printf("[SearchBookByISBNHandler] 孔夫子 API 查询失败: %v", err) var redisBook request.BookInfoByPsi
// c.JSON(500, gin.H{"error": err.Error()}) if err := json.Unmarshal([]byte(val), &redisBook); err == nil {
// return esBook := ConvertRedisBookToESBookByPsi(&redisBook)
// } if esBook != nil {
// if apiBook == nil || apiBook.Data.ISBN == "" { responseData := esBook.ConvertToResponseByPsi()
// log.Printf("[SearchBookByISBNHandler] 孔夫子 API 未找到图书信息 ISBN: %s", isbn) c.JSON(200, gin.H{
// c.JSON(404, gin.H{"error": "未找到图书信息"}) "data": responseData,
// return })
// } return
// }
// log.Printf("[SearchBookByISBNHandler] 获取到图书信息: %+v", apiBook.Data) } else {
// log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err)
// 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) // ES 查询(使用监控)
// } else { query := map[string]interface{}{
// pddBookPicURL = url "query": map[string]interface{}{
// log.Printf("[SearchBookByISBNHandler] 上传 book_pic 成功: %s", url) "term": map[string]interface{}{
// } "isbn": isbn,
// } },
// },
// pddBookPicSURL := "" "_source": true,
// if apiBook.Data.BookPicS != "" { }
// url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPicS, isbn, "true", apiBook.Data.BookName, "true")
// if err != nil { body, err := json.Marshal(query)
// log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 失败: %v", err) if err != nil {
// } else { log.Printf("[SearchBookByISBNHandler] 构建查询 JSON 失败:%v", err)
// pddBookPicSURL = url c.JSON(500, gin.H{"error": "构建查询失败"})
// log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 成功: %s", url) return
// } }
// }
// req := esapi.SearchRequest{
// esBook := ConvertKongfzToESBook(apiBook) Index: []string{ESIndex},
// Body: bytes.NewReader(body),
// esBook.BookPicS.PddResponse = pddBookPicSURL TrackTotalHits: true,
// esBook.BookPic.PddPath = pddBookPicURL Pretty: true,
// //log.Printf("[SearchBookByISBNHandler] 写入 ES: %+v", esBook) }
//
// result, err = svc.AddBookToES(c.Request.Context(), esBook) // 创建监控客户端并执行查询
// if err != nil { monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint)
// log.Printf("[SearchBookByISBNHandler] 写入 ES 失败: %v", err) resp, duration, err := monitoredES.Search(ctx, &req)
// c.JSON(500, gin.H{"error": err.Error()})
// return log.Printf("[SearchBookByISBNHandler] ES 查询耗时:%dms", duration.Milliseconds())
// }
// if err != nil {
// log.Printf("[SearchBookByISBNHandler] 写入 ES 成功, ISBN: %s", isbn) log.Printf("[SearchBookByISBNHandler] ES 查询失败:%v", err)
// } else { c.JSON(500, gin.H{"error": "ES 查询失败:" + err.Error()})
// //log.Printf("[SearchBookByISBNHandler] 从 ES 查询到图书: %+v", result) return
// } }
// defer resp.Body.Close()
// responseData := result.ConvertToResponse()
// c.JSON(200, gin.H{ if resp.IsError() {
// "data": responseData, 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 {
c.JSON(200, gin.H{
"data": nil,
})
return
}
responseData := result.ConvertToResponseByPsi()
c.JSON(200, gin.H{
"data": responseData,
})
}
// ConvertKongfzToESBook 将第三方接口返回的数据转换为 ESBook 结构 // ConvertKongfzToESBook 将第三方接口返回的数据转换为 ESBook 结构
func ConvertKongfzToESBook(apiBook *kongfz.BookResponse) *ESBook { func ConvertKongfzToESBook(apiBook *kongfz.BookResponse) *ESBook {
@ -2821,8 +3043,6 @@ func (svc *ESSearchService) UpdateBookSuitByISBNHandler(c *gin.Context) {
}, },
} }
fmt.Println(body)
payload, _ := json.Marshal(body) payload, _ := json.Marshal(body)
res, err := svc.ES.Client.UpdateByQuery( res, err := svc.ES.Client.UpdateByQuery(
@ -2910,7 +3130,6 @@ func (svc *ESSearchService) UpdateBookFieldsByISBNHandler(c *gin.Context) {
// 先确认 ISBN 是否存在 // 先确认 ISBN 是否存在
book, err := svc.SearchBookByISBN(isbn) book, err := svc.SearchBookByISBN(isbn)
fmt.Println("book:", book)
if err != nil { if err != nil {
c.JSON(500, gin.H{ c.JSON(500, gin.H{
"error": "查询ES失败", "error": "查询ES失败",
@ -3019,7 +3238,6 @@ func (svc *ESSearchService) UpdateBookFieldsByISBN(
fmt.Sprintf("ctx._source.%s = params.%s;", field, field)) fmt.Sprintf("ctx._source.%s = params.%s;", field, field))
params[field] = value params[field] = value
} }
fmt.Println("scriptParts:", scriptParts)
if len(scriptParts) == 0 { if len(scriptParts) == 0 {
return 0, fmt.Errorf("没有有效的字段可更新") return 0, fmt.Errorf("没有有效的字段可更新")
} }
@ -3626,6 +3844,10 @@ func (svc *ESSearchService) AddBookToES(ctx context.Context, req *ESBook) (*ESBo
return nil, fmt.Errorf("ES返回错误: %s", res.String()) return nil, fmt.Errorf("ES返回错误: %s", res.String())
} }
// 同步 Redis
if svc.SyncRedisByISBN != nil {
_ = svc.SyncRedisByISBN(req.ISBN, "update")
}
return &newBook, nil return &newBook, nil
} }
@ -4266,3 +4488,77 @@ func ConvertRedisBookToESBook(redisBook *request.BookInfo) *ESBook {
IsFilter: "000000", IsFilter: "000000",
} }
} }
// ConvertRedisBookToESBookByPsi 将 Redis 中的 BookInfo 转换为 ESBook
func ConvertRedisBookToESBookByPsi(redisBook *request.BookInfoByPsi) *ESBook {
if redisBook == nil || redisBook.Isbn == "" {
return nil
}
// 构建图片对象
carouselUrls := []string{}
if len(redisBook.ImageObject.CarouselUrlArray) > 0 {
carouselUrls = redisBook.ImageObject.CarouselUrlArray
}
liveShootingUrls := []string{}
if len(redisBook.ImageObject.DetailUrlObject.LiveShootingUrl) > 0 {
liveShootingUrls = redisBook.ImageObject.DetailUrlObject.LiveShootingUrl
}
// 提取第一张轮播图作为 book_pic
bookPicPath := ""
if len(carouselUrls) > 0 {
bookPicPath = carouselUrls[0]
}
// 提取第一张实拍图作为 book_pic_s
bookPicSResponse := ""
if len(liveShootingUrls) > 0 {
bookPicSResponse = liveShootingUrls[0]
}
return &ESBook{
ISBN: redisBook.Isbn,
BookName: FlexibleString{Value: redisBook.BookName},
Author: redisBook.Author,
Publisher: redisBook.Publishing,
PublicationTime: redisBook.PublicationDate,
BindingLayout: redisBook.Binding,
PageCount: NumberOrString(strconv.FormatInt(redisBook.PagesCount, 10)),
WordCount: NumberOrString(strconv.FormatInt(redisBook.WordsCount, 10)),
BookFormat: NumberOrString(strconv.FormatInt(redisBook.Format, 10)),
FixPrice: Float64OrString(redisBook.Price),
BookPic: BookPicObj{
LocalPath: "",
PddPath: bookPicPath,
},
BookPicS: BookPicSObj{
LocalPath: "",
PddResponse: bookPicSResponse,
},
BookDefPic: BookDefPicObj{
LocalPath: "",
PddPath: redisBook.ImageObject.DefaultImageUrl,
},
BookPicB: redisBook.ImageObject.WhiteBackgroundUrl,
CatId: redisBook.CatIdObject,
// 销量等字段默认为 0
DaySale7: 0,
DaySale15: 0,
DaySale30: 0,
DaySale60: 0,
DaySale90: 0,
DaySale180: 0,
DaySale365: 0,
ThisYearSale: 0,
LastYearSale: 0,
TotalSale: 0,
BuyCounts: 0,
SellCounts: 0,
IsSuit: redisBook.IsSuit,
IsIllegal: 0,
IsReturn: 0,
IsFilter: "000000",
}
}

34
main.go
View File

@ -163,31 +163,18 @@ func main() {
//} //}
//defer bookCenter.db.Close() //defer bookCenter.db.Close()
// ======================= 新增:初始化 ES ============================ // ======================= 新增:初始化 ES ============================
/////** 旧ESES0 **/ /** 本地测试公网IP **/
//esClient, err := es.NewESClient( //esClient, err := es.NewESClient(
// []string{"http://103.236.91.138:9200"}, // []string{"http://36.212.12.92:9527"},
// "elastic", // "elastic",
// "5mRDIUg52VC0fp14nw-F", // "+Tz5qR_KushZ-bPgZ_H-",
//) //)
///** ESES1 **/ /** 线上使用内网IP **/
//esClient, err := es.NewESClient(
// []string{"http://103.236.74.207:9200"},
// "elastic",
// "y3h2fdYyrXewkSrY6uhh",
//)
/** 新ES任务2 **/
//esClient, err := es.NewESClient(
// []string{"http://localhost:9200"},
// "elastic",
// "zDzSXel3PFwx9=6Ybmqv",
//)
/** ES2 本地测试 **/
esClient, err := es.NewESClient( esClient, err := es.NewESClient(
[]string{"http://36.212.1.63:9200"}, []string{"http://192.168.1.211:9527"},
"elastic", "elastic",
"zDzSXel3PFwx9=6Ybmqv", "+Tz5qR_KushZ-bPgZ_H-",
) )
if err != nil { if err != nil {
@ -197,7 +184,7 @@ func main() {
esService := es.NewESSearchService(esClient) esService := es.NewESSearchService(esClient)
redisClient.AddClient("db4", "36.212.12.247:6379", "long6166@@", 2) redisClient.AddClient("db4", "36.212.12.247:6379", "long6166@@", 2)
redisClient.AddClient("db1", "36.212.12.247:6379", "long6166@@", 1) redisClient.AddClient("db1", "36.212.12.247:6379", "long6166@@", 7)
//redisClient.AddClient("test", "127.0.0.1:6379", "", 0) //redisClient.AddClient("test", "127.0.0.1:6379", "", 0)
// =================================================================== // ===================================================================
@ -306,6 +293,10 @@ func main() {
r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler) //监控 r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler) //监控
// ISBN 精确搜索 // ISBN 精确搜索
r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) //监控 r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) //监控
// ISBN 精确搜索 为咸鱼商品下架回调
r.GET("/api/es/searchByISBNByXyCallBack", esService.SearchByISBNByXyCallBack) //监控
// ISBN 精确搜索 为psi提供
r.GET("/api/es/searchByISBNtoPsi", esService.SearchBookByISBNHandlerToPsi) //监控
// 书名搜索 // 书名搜索
r.GET("/api/es/searchByBookName", esService.SearchBookByBookNameHandler) r.GET("/api/es/searchByBookName", esService.SearchBookByBookNameHandler)
// 全字段搜索 // 全字段搜索
@ -316,6 +307,7 @@ func main() {
//------------------------------------------------------------------------ //------------------------------------------------------------------------
// 初始化控制器 新 // 初始化控制器 新
bookSearchService := service.NewBookService(esClient) bookSearchService := service.NewBookService(esClient)
esService.SyncRedisByISBN = bookSearchService.SyncRedisByISBN
bookController := controller.NewBookController(bookSearchService) bookController := controller.NewBookController(bookSearchService)
// 根据条件查询 ES 图书信息 // 根据条件查询 ES 图书信息
r.GET("/api/es/getBookBaseInfoES", bookController.SearchBookBaseInfoHandler) r.GET("/api/es/getBookBaseInfoES", bookController.SearchBookBaseInfoHandler)
@ -323,7 +315,7 @@ func main() {
r.POST("/api/es/addBookToES", bookController.AddBookToESHandler) r.POST("/api/es/addBookToES", bookController.AddBookToESHandler)
// 更新根据ISBN通用更新图书字段 // 更新根据ISBN通用更新图书字段
r.POST("/api/es/updateBookFieldsByISBN", bookController.UpdateBookFieldsByISBNHandler) r.POST("/api/es/updateBookFieldsByISBN", bookController.UpdateBookFieldsByISBNHandler)
// 更新根据ISBN通用更新图书字段 // 更新根据ISBN更新商品分类字段
r.POST("/api/es/updateBookCatIdByISBN", bookController.UpdateBookCatIdByISBNHandler) r.POST("/api/es/updateBookCatIdByISBN", bookController.UpdateBookCatIdByISBNHandler)
// 删除根据ISBN删除ES数据 // 删除根据ISBN删除ES数据
r.GET("/api/es/DeleteBookByISBN", bookController.DeleteBookHandler) r.GET("/api/es/DeleteBookByISBN", bookController.DeleteBookHandler)

1673
md/API.md

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,10 @@ type BookSearchRequest struct {
LastYearSale string `form:"last_year_sale"` // 去年销量 LastYearSale string `form:"last_year_sale"` // 去年销量
TotalSaleRange string `form:"totalSale_range"` // 总销量范围 TotalSaleRange string `form:"totalSale_range"` // 总销量范围
ID string `form:"id"` // ID ID string `form:"id"` // ID
PageCount string `form:"page_count"` // 页数
WordCount string `form:"word_count"` // 字数
KongfzCategories string `form:"kongfz_categories"` // 孔夫子分类列表,多个用逗号分隔
KongfzInclude int8 `form:"kongfz_include"` // 是否涵盖1-是2-否
} }
// BookUpdateRequest 更新图书请求参数 // BookUpdateRequest 更新图书请求参数
@ -68,6 +72,23 @@ type BookInfo struct {
CatIdObject CatIdObject `json:"cat_id"` // 分类 CatIdObject CatIdObject `json:"cat_id"` // 分类
} }
// BookInfoByPsi 书籍信息结构
type BookInfoByPsi struct {
Isbn string `json:"isbn"` // ISBN
BookName string `json:"book_name"` // 书名
Author string `json:"author"` // 作者
Publishing string `json:"publishing"` // 出版社
PublicationDate string `json:"publication_date"` // 出版时间
Binding string `json:"binding"` // 装帧
PagesCount int64 `json:"pages_count"` // 页数
WordsCount int64 `json:"words_count"` // 字数
Format int64 `json:"format"` // 开本
ImageObject *ImageObject `json:"image_object"` // 图片
Price int64 `json:"price"` // 售价
CatIdObject CatIdObject `json:"cat_id"` // 分类
IsSuit int `json:"is_suit"`
}
// ImageObject 图片对象结构 // ImageObject 图片对象结构
type ImageObject struct { type ImageObject struct {
CarouselUrlArray []string `json:"carousel_url_array"` // 轮播图 CarouselUrlArray []string `json:"carousel_url_array"` // 轮播图

View File

@ -6,18 +6,18 @@ import (
"centerBook/es" "centerBook/es"
"centerBook/model/request" "centerBook/model/request"
"centerBook/tail" "centerBook/tail"
"centerBook/util"
"centerBook/util/redisClient" "centerBook/util/redisClient"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/elastic/go-elasticsearch/v8/esapi"
jsoniter "github.com/json-iterator/go"
"io" "io"
"log" "log"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/elastic/go-elasticsearch/v8/esapi"
jsoniter "github.com/json-iterator/go"
) )
// BookService 图书搜索服务 // BookService 图书搜索服务
@ -122,6 +122,7 @@ func (svc *BookService) SearchBookBaseInfo(request *request.BookSearchRequest) (
svc.buildIsFilterCondition(queryBuilder, request.IsFilter, request.ShopType) svc.buildIsFilterCondition(queryBuilder, request.IsFilter, request.ShopType)
svc.buildCategoryTypeCondition(queryBuilder, request.CategoryType) svc.buildCategoryTypeCondition(queryBuilder, request.CategoryType)
svc.buildCategoryCondition(queryBuilder, request.Category) svc.buildCategoryCondition(queryBuilder, request.Category)
svc.buildKongfzCategoryCondition(queryBuilder, request.KongfzCategories, request.KongfzInclude)
svc.buildBookPicCondition(queryBuilder, request.BookPic, request.PicType) svc.buildBookPicCondition(queryBuilder, request.BookPic, request.PicType)
svc.buildBuyCountsCondition(queryBuilder, request.BuyCounts, saleField) svc.buildBuyCountsCondition(queryBuilder, request.BuyCounts, saleField)
svc.buildTotalSaleRangeCondition(queryBuilder, request.TotalSaleRange) svc.buildTotalSaleRangeCondition(queryBuilder, request.TotalSaleRange)
@ -268,22 +269,44 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
var scriptParts []string var scriptParts []string
params := make(map[string]interface{}) params := make(map[string]interface{})
// 判断 is_suit 是否已传递,如果没传则自动检测 //svc.AddFilterSet(request.ISBN)
if _, exists := request.Data["is_suit"]; !exists { // 先捕获 is_suit 的值v3 同步/删除在 v2 更新完成后执行
// 未传递 is_suit自动检测并设置 var hasIsSuitInData bool
params["is_suit"] = map[bool]int{true: 1, false: 0}[es.CheckBookSuit(book.BookName.Value)] var isSuitInData interface{}
scriptParts = append(scriptParts, fmt.Sprintf("ctx._source.is_suit = params.is_suit;")) if val, exists := request.Data["is_suit"]; exists {
hasIsSuitInData = true
isSuitInData = val
} }
for field, value := range request.Data { for field, value := range request.Data {
// 使用配置检查字段是否允许更新 // 使用配置检查字段是否允许更新
if !fieldConfig.IsAllowUpdate(field) { if !fieldConfig.IsAllowUpdate(field) {
log.Printf("[UpdateBookFieldsByISBN] 字段 %s 不允许更新,已跳过", field) fmt.Printf("[UpdateBookFieldsByISBN] 字段 %s 不允许更新,已跳过", field)
continue continue
} }
// 对 publication_time 字段做特殊处理
if field == "publication_time" {
// 将原始值转换为 int64 并加上 5364000000
var pubTime int64
switch v := value.(type) {
case int:
pubTime = int64(v)
case int64:
pubTime = v
case float64:
pubTime = int64(v)
case string:
// 如果是字符串,尝试解析
if parsed, err := strconv.ParseInt(v, 10, 64); err == nil {
pubTime = parsed
}
}
pubTime += 5364000000
params[field] = strconv.FormatInt(pubTime, 10)
} else {
params[field] = value
}
scriptParts = append(scriptParts, scriptParts = append(scriptParts,
fmt.Sprintf("ctx._source.%s = params.%s;", field, field)) fmt.Sprintf("ctx._source.%s = params.%s;", field, field))
params[field] = value
} }
if len(scriptParts) == 0 { if len(scriptParts) == 0 {
return nil, fmt.Errorf("没有有效的更新字段") return nil, fmt.Errorf("没有有效的更新字段")
@ -300,6 +323,7 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
}, },
}, },
} }
fmt.Println("[DEBUG] ES UpdateByQuery Request Body:", body)
payload, _ := json.Marshal(body) payload, _ := json.Marshal(body)
res, err := svc.esClient.Client.UpdateByQuery( res, err := svc.esClient.Client.UpdateByQuery(
[]string{es.ESIndex}, []string{es.ESIndex},
@ -330,7 +354,18 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
// 同步 Redis // 同步 Redis
_ = svc.SyncRedisByISBN(request.ISBN, "update") _ = svc.SyncRedisByISBN(request.ISBN, "update")
// v2 已更新完成,此时操作 v3 索引
if hasIsSuitInData {
if isOneValue(isSuitInData) {
if err := svc.syncBookToV3Index(request.ISBN); err != nil {
log.Printf("[WARN] 同步到v3索引失败: %v", err)
}
} else if isZeroValue(isSuitInData) {
if err := svc.deleteBookFromV3Index(request.ISBN); err != nil {
log.Printf("[WARN] 从v3索引删除失败: %v", err)
}
}
}
return &UpdateBookResult{ return &UpdateBookResult{
ISBN: request.ISBN, ISBN: request.ISBN,
Updated: parsed.Updated, Updated: parsed.Updated,
@ -338,6 +373,113 @@ func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateReques
}, nil }, nil
} }
// AddFilterSet 添加过滤集合POST form-data
//func (svc *BookService) AddFilterSet(isbn string) error {
// fmt.Println("AddFilterSet start")
// url := "https://erp.buzhiyushu.cn/zhishu/filterSet"
//
// // 创建 JSON 请求体
// jsonData := map[string]interface{}{
// "filterType": "1",
// "limitationType": "0",
// "addWay": "0",
// "addTxt": isbn,
// "sort": "0,3",
// }
//
// jsonBody, err := json.Marshal(jsonData)
// if err != nil {
// return fmt.Errorf("序列化 JSON 失败:%v", err)
// }
//
// fmt.Println("[AddFilterSet] 创建 JSON 请求体", isbn)
//
// // 创建 HTTP 请求
// httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBody))
// if err != nil {
// return fmt.Errorf("创建 HTTP 请求失败:%v", err)
// }
//
// // 设置请求头
// httpReq.Header.Set("Content-Type", "application/json")
//
// // 发送请求
// client := &http.Client{
// Timeout: 30 * time.Second,
// }
//
// resp, err := client.Do(httpReq)
// if err != nil {
// return fmt.Errorf("发送 HTTP 请求失败:%v", err)
// }
// fmt.Println("[AddFilterSet] 响应状态:%d", resp.StatusCode)
// defer resp.Body.Close()
//
// // 读取响应
// respBody, err := io.ReadAll(resp.Body)
// if err != nil {
// return fmt.Errorf("读取响应失败:%v", err)
// }
//
// log.Printf("[AddFilterSet] 响应状态:%d | 响应内容:%s", resp.StatusCode, string(respBody))
//
// return nil
//}
// AddFilterSet 添加过滤集合POST form-data
//func (svc *BookService) AddFilterSet(isbn string) error {
// fmt.Println("AddFilterSet start")
// url := "http://103.236.91.138:8888/api/addFilterSet"
//
// // 创建 multipart form
// body := &bytes.Buffer{}
// writer := multipart.NewWriter(body)
// fmt.Println("[AddFilterSet] 创建 multipart form", isbn)
// // 添加表单字段
// _ = writer.WriteField("filterType", "1")
// _ = writer.WriteField("limitationType", "0")
// _ = writer.WriteField("addWay", "6")
// _ = writer.WriteField("addTxt", isbn)
// _ = writer.WriteField("createBy", "1")
// _ = writer.WriteField("tenantId", "000000")
// _ = writer.WriteField("sort", "0,3")
//
// err := writer.Close()
// if err != nil {
// return fmt.Errorf("关闭 multipart writer 失败:%v", err)
// }
//
// // 创建 HTTP 请求
// httpReq, err := http.NewRequest("POST", url, body)
// if err != nil {
// return fmt.Errorf("创建 HTTP 请求失败:%v", err)
// }
//
// httpReq.Header.Set("Content-Type", writer.FormDataContentType())
// // 发送请求
// client := &http.Client{
// Timeout: 30 * time.Second,
// }
//
// resp, err := client.Do(httpReq)
// fmt.Println("[AddFilterSet] 发送 HTTP 请求", resp)
// if err != nil {
// return fmt.Errorf("发送 HTTP 请求失败:%v", err)
// }
// fmt.Println("[AddFilterSet] 响应状态:%d", resp.StatusCode)
// defer resp.Body.Close()
//
// // 读取响应
// respBody, err := io.ReadAll(resp.Body)
// if err != nil {
// return fmt.Errorf("读取响应失败:%v", err)
// }
//
// log.Printf("[AddFilterSet] 响应状态:%d | 响应内容:%s", resp.StatusCode, string(respBody))
//
// return nil
//}
// UpdateBookCatIdByISBNHandler 更新图书字段 // UpdateBookCatIdByISBNHandler 更新图书字段
func (svc *BookService) UpdateBookCatIdByISBNHandler(request *request.BookUpdateRequest) (*UpdateBookResult, error) { func (svc *BookService) UpdateBookCatIdByISBNHandler(request *request.BookUpdateRequest) (*UpdateBookResult, error) {
// 先确认 ISBN 是否存在 // 先确认 ISBN 是否存在
@ -354,13 +496,30 @@ func (svc *BookService) UpdateBookCatIdByISBNHandler(request *request.BookUpdate
return nil, fmt.Errorf("没有有效的更新字段") return nil, fmt.Errorf("没有有效的更新字段")
} }
// 构建更新脚本,只更新传入的字段,保留现有字段
var scriptParts []string
params := make(map[string]interface{})
// 转换为 map 以便访问
newCatId, ok := catIdValue.(map[string]interface{})
if ok {
for key, value := range newCatId {
scriptParts = append(scriptParts, fmt.Sprintf("ctx._source.cat_id.%s = params.%s;", key, key))
params[key] = value
}
}
if len(scriptParts) == 0 {
return nil, fmt.Errorf("没有有效的 cat_id 字段")
}
script := strings.Join(scriptParts, " ")
body := map[string]interface{}{ body := map[string]interface{}{
"script": map[string]interface{}{ "script": map[string]interface{}{
"source": "ctx._source.cat_id = params.cat_id;", "source": script,
"lang": "painless", "lang": "painless",
"params": map[string]interface{}{ "params": params,
"cat_id": catIdValue,
},
}, },
"query": map[string]interface{}{ "query": map[string]interface{}{
"term": map[string]interface{}{ "term": map[string]interface{}{
@ -584,7 +743,10 @@ func (svc *BookService) addBookToES(ctx context.Context, req *es.ESBook) (*es.ES
// 处理出版时间,转换为时间戳 // 处理出版时间,转换为时间戳
publicationTimeTimestamp := req.PublicationTime publicationTimeTimestamp := req.PublicationTime
if req.PublicationTime != "" && req.PublicationTime != "0" { if req.PublicationTime != "" && req.PublicationTime != "0" {
publicationTimeTimestamp = strconv.FormatInt(util.ParsePublicationTime(publicationTimeTimestamp), 10) publicationTimeIn64, err := strconv.ParseInt(req.PublicationTime, 10, 64)
if err == nil {
publicationTimeTimestamp = strconv.FormatInt(publicationTimeIn64+5364000000, 10)
}
} }
// 准备数据映射(使用 Go 字段名) // 准备数据映射(使用 Go 字段名)
dataMap := make(map[string]interface{}) dataMap := make(map[string]interface{})
@ -633,7 +795,6 @@ func (svc *BookService) addBookToES(ctx context.Context, req *es.ESBook) (*es.ES
if req.Other != nil { if req.Other != nil {
dataMap["Other"] = req.Other dataMap["Other"] = req.Other
} }
fmt.Println("dataMap:", dataMap)
// 使用配置构建 ES 文档(自动转换字段名) // 使用配置构建 ES 文档(自动转换字段名)
doc := es.GetESFieldConfig().BuildESDocument(dataMap) doc := es.GetESFieldConfig().BuildESDocument(dataMap)
@ -726,7 +887,7 @@ func (svc *BookService) SyncRedisByISBN(isbn string, act string) error {
if book.PublicationTime != "" && book.PublicationTime != "0" { if book.PublicationTime != "" && book.PublicationTime != "0" {
publicationTimeIn64, err := strconv.ParseInt(book.PublicationTime, 10, 64) publicationTimeIn64, err := strconv.ParseInt(book.PublicationTime, 10, 64)
if err == nil { if err == nil {
redisBookInfo.PublicationDate = time.Unix(publicationTimeIn64, 0).Format("2006-01") redisBookInfo.PublicationDate = time.Unix(publicationTimeIn64-5364000000, 0).Format("2006-01")
} }
} }
if book.BindingLayout != "" { if book.BindingLayout != "" {
@ -901,6 +1062,199 @@ func (svc *BookService) indexDocumentToES(ctx context.Context, doc map[string]in
return nil return nil
} }
// isOneValue 判断 interface{} 类型的值是否为 1
func isOneValue(v interface{}) bool {
switch val := v.(type) {
case int:
return val == 1
case int8:
return val == 1
case int16:
return val == 1
case int32:
return val == 1
case int64:
return val == 1
case float32:
return val == 1.0
case float64:
return val == 1.0
case string:
return val == "1"
default:
return false
}
}
// isZeroValue 判断 interface{} 类型的值是否为 0
func isZeroValue(v interface{}) bool {
switch val := v.(type) {
case int:
return val == 0
case int8:
return val == 0
case int16:
return val == 0
case int32:
return val == 0
case int64:
return val == 0
case float32:
return val == 0.0
case float64:
return val == 0.0
case string:
return val == "0"
default:
return false
}
}
// searchBookByISBNInIndex 在指定索引中按 ISBN 查询文档
func (svc *BookService) searchBookByISBNInIndex(isbn string, index string) (*es.ESBook, error) {
log.Printf("[SearchBookByISBNInIndex] 开始查询 | ISBN=%s | index=%s", isbn, index)
query := map[string]interface{}{
"query": map[string]interface{}{
"term": map[string]interface{}{
"isbn": isbn,
},
},
"_source": true,
"size": 1,
}
body, err := json.Marshal(query)
if err != nil {
log.Printf("[SearchBookByISBNInIndex] 构建查询 JSON 失败:%v", err)
return nil, fmt.Errorf("构建查询 JSON 失败:%v", err)
}
res, err := svc.esClient.Client.Search(
svc.esClient.Client.Search.WithIndex(index),
svc.esClient.Client.Search.WithBody(bytes.NewReader(body)),
svc.esClient.Client.Search.WithTrackTotalHits(true),
)
if err != nil {
log.Printf("[SearchBookByISBNInIndex] ES 查询失败:%v", err)
return nil, fmt.Errorf("ES 查询失败:%v", err)
}
defer res.Body.Close()
if res.IsError() {
log.Printf("[SearchBookByISBNInIndex] ES 返回错误:%s", res.String())
return nil, fmt.Errorf("ES 返回错误:%s", res.String())
}
var parsed esHitsWrapper
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
log.Printf("[SearchBookByISBNInIndex] 解析 ES 响应失败:%v", err)
return nil, fmt.Errorf("解析 ES 响应失败:%v", err)
}
if len(parsed.Hits.Hits) == 0 {
log.Printf("[SearchBookByISBNInIndex] 未找到 ISBN=%s 对应文档", isbn)
return nil, nil
}
book := parsed.Hits.Hits[0].Source
log.Printf("[SearchBookByISBNInIndex] 查询到文档: %+v", book)
return &book, nil
}
// indexDocumentToIndex 将文档写入指定索引(幂等:存在则覆盖,不存在则创建)
func (svc *BookService) indexDocumentToIndex(ctx context.Context, doc map[string]interface{}, id string, index string) error {
jsonData, _ := json.Marshal(doc)
esReq := esapi.IndexRequest{
Index: index,
DocumentID: id,
Body: bytes.NewReader(jsonData),
Refresh: "true",
}
res, err := esReq.Do(ctx, svc.esClient.Client.Transport)
if err != nil {
return fmt.Errorf("写入索引 %s 失败:%w", index, err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("写入索引 %s 错误:%s", index, res.String())
}
log.Printf("[IndexDocumentToIndex] 成功 | index=%s | id=%s", index, id)
return nil
}
// syncBookToV3Index 将 v2 索引中的文档同步到 v3 索引
// 先检查 v3 中是否已存在同 ISBN 文档,存在则覆盖更新,不存在则新增
func (svc *BookService) syncBookToV3Index(isbn string) error {
// 1. 从 v2 索引查询完整文档
book, err := svc.SearchBookByISBN(isbn)
if err != nil {
return fmt.Errorf("查询v2索引失败: %w", err)
}
if book == nil {
return fmt.Errorf("v2索引中未找到ISBN=%s的文档", isbn)
}
// 2. 构建文档,追加 v3 专有字段
doc := svc.buildBookMapForSerialization(book)
doc["fid"] = 0
// 3. 检查 v3 中是否已存在
existing, err := svc.searchBookByISBNInIndex(isbn, es.ESIndexV3)
if err != nil {
return fmt.Errorf("查询v3索引失败: %w", err)
}
ctx := context.Background()
if existing != nil {
log.Printf("[SyncToV3] ISBN=%s 在v3中已存在执行覆盖更新", isbn)
} else {
log.Printf("[SyncToV3] ISBN=%s 在v3中不存在执行新增", isbn)
}
// 4. 写入 v3 索引IndexRequest 是幂等的:存在则覆盖,不存在则创建)
return svc.indexDocumentToIndex(ctx, doc, isbn, es.ESIndexV3)
}
// deleteBookFromV3Index 从 v3 索引中删除指定 ISBN 的文档
func (svc *BookService) deleteBookFromV3Index(isbn string) error {
// 先检查 v3 中是否存在
existing, err := svc.searchBookByISBNInIndex(isbn, es.ESIndexV3)
if err != nil {
return fmt.Errorf("查询v3索引失败: %w", err)
}
if existing == nil {
log.Printf("[DeleteFromV3] ISBN=%s 在v3中不存在无需删除", isbn)
return nil
}
ctx := context.Background()
req := esapi.DeleteRequest{
Index: es.ESIndexV3,
DocumentID: isbn,
Refresh: "true",
}
res, err := req.Do(ctx, svc.esClient.Client.Transport)
if err != nil {
return fmt.Errorf("从v3索引删除失败: %w", err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("从v3索引删除错误: %s", res.String())
}
log.Printf("[DeleteFromV3] 成功删除 ISBN=%s 从v3索引", isbn)
return nil
}
// GetLastID 获取最后一条 ID // GetLastID 获取最后一条 ID
func (svc *BookService) GetLastID() (int, error) { func (svc *BookService) GetLastID() (int, error) {
query := `{ query := `{
@ -1062,7 +1416,6 @@ func (svc *BookService) buildIsFilterCondition(builder *ESQueryBuilder, isFilter
default: default:
return return
} }
builder.AddQuery(&QueryCondition{ builder.AddQuery(&QueryCondition{
Field: "is_filter", Field: "is_filter",
Type: "wildcard", Type: "wildcard",
@ -1160,6 +1513,7 @@ func (svc *BookService) buildBuyCountsCondition(builder *ESQueryBuilder, buyCoun
if len(parts) == 2 { if len(parts) == 2 {
minVal, _ := strconv.Atoi(parts[0]) minVal, _ := strconv.Atoi(parts[0])
maxVal, _ := strconv.Atoi(parts[1]) maxVal, _ := strconv.Atoi(parts[1])
// 普通范围查询,不包含 null 值
builder.AddQuery(&QueryCondition{ builder.AddQuery(&QueryCondition{
Field: saleField, Field: saleField,
Type: "range", Type: "range",
@ -1187,6 +1541,7 @@ func (svc *BookService) buildTotalSaleRangeCondition(builder *ESQueryBuilder, to
if len(parts) == 2 { if len(parts) == 2 {
minVal, _ := strconv.Atoi(parts[0]) minVal, _ := strconv.Atoi(parts[0])
maxVal, _ := strconv.Atoi(parts[1]) maxVal, _ := strconv.Atoi(parts[1])
// 普通范围查询,不包含 null 值
builder.AddQuery(&QueryCondition{ builder.AddQuery(&QueryCondition{
Field: "total_sale", Field: "total_sale",
Type: "range", Type: "range",
@ -1210,6 +1565,8 @@ func (svc *BookService) buildNumericRangeConditions(builder *ESQueryBuilder, req
"ThisYearSale": request.ThisYearSale, "ThisYearSale": request.ThisYearSale,
"LastYearSale": request.LastYearSale, "LastYearSale": request.LastYearSale,
"PublicationTime": request.PublicationTime, "PublicationTime": request.PublicationTime,
"PageCount": request.PageCount,
"WordCount": request.WordCount,
} }
esFields := map[string]string{ esFields := map[string]string{
@ -1224,6 +1581,8 @@ func (svc *BookService) buildNumericRangeConditions(builder *ESQueryBuilder, req
"ThisYearSale": "this_year_sale", "ThisYearSale": "this_year_sale",
"LastYearSale": "last_year_sale", "LastYearSale": "last_year_sale",
"PublicationTime": "publication_time", "PublicationTime": "publication_time",
"PageCount": "page_count",
"WordCount": "word_count",
} }
for fieldName, value := range fields { for fieldName, value := range fields {
@ -1232,36 +1591,13 @@ func (svc *BookService) buildNumericRangeConditions(builder *ESQueryBuilder, req
} }
esField := esFields[fieldName] esField := esFields[fieldName]
parts := strings.Split(value, ",") parts := strings.Split(value, ",")
fmt.Println("ppp", fieldName, parts)
if len(parts) == 2 { if len(parts) == 2 {
minVal, _ := strconv.Atoi(parts[0]) minVal, _ := strconv.Atoi(parts[0])
maxVal, _ := strconv.Atoi(parts[1]) maxVal, _ := strconv.Atoi(parts[1])
// 如果查询范围包含 00-999999需要同时匹配 null 值 if fieldName == "PublicationTime" {
if minVal == 0 { minVal += 5364000000
// 使用 bool should 查询:匹配范围内值 OR 字段为 null/不存在 maxVal += 5364000000
rangeQuery := map[string]interface{}{
"range": map[string]interface{}{
esField: map[string]interface{}{
"gte": minVal,
"lte": maxVal,
},
},
} }
nullQuery := map[string]interface{}{
"bool": map[string]interface{}{
"must_not": []map[string]interface{}{
{
"exists": map[string]interface{}{
"field": esField,
},
},
},
},
}
builder.AddBoolQuery("should", []map[string]interface{}{rangeQuery, nullQuery})
} else {
// 普通范围查询,不包含 null 值 // 普通范围查询,不包含 null 值
builder.AddQuery(&QueryCondition{ builder.AddQuery(&QueryCondition{
Field: esField, Field: esField,
@ -1270,13 +1606,6 @@ func (svc *BookService) buildNumericRangeConditions(builder *ESQueryBuilder, req
LTE: maxVal, LTE: maxVal,
}) })
} }
//builder.AddQuery(&QueryCondition{
// Field: esField,
// Type: "range",
// GTE: minVal,
// LTE: maxVal,
//})
}
} }
} }
@ -1358,6 +1687,74 @@ func (svc *BookService) buildDefaultPrefixConditions(builder *ESQueryBuilder, re
} }
} }
// buildKongfzCategoryCondition 构建孔夫子分类查询条件
func (svc *BookService) buildKongfzCategoryCondition(builder *ESQueryBuilder, kongfzCategories string, kongfzInclude int8) {
if kongfzCategories == "" {
return
}
log.Printf("[DEBUG] kongfz_categories=%s, kongfz_include=%s", kongfzCategories, kongfzInclude)
// 分割多个分类ID
categories := strings.Split(kongfzCategories, ",")
if len(categories) == 0 {
return
}
// 过滤空值
var validCategories []string
for _, catID := range categories {
catID = strings.TrimSpace(catID)
if catID != "" {
validCategories = append(validCategories, catID)
}
}
if len(validCategories) == 0 {
return
}
// 根据 kongfzInclude 参数决定查询逻辑
if kongfzInclude == 2 {
// 否查询不包含这些分类的数据must_not
var mustNotQueries []map[string]interface{}
for _, catID := range validCategories {
mustNotQueries = append(mustNotQueries, map[string]interface{}{
"prefix": map[string]interface{}{
"cat_id.kong_fu_zi_cat_id": catID,
},
})
}
if len(mustNotQueries) > 0 {
builder.AddBoolQuery("must_not", mustNotQueries)
log.Printf("[DEBUG] Added %d kongfz category exclude queries (must_not)", len(mustNotQueries))
}
} else {
// 是查询包含这些分类的数据should + minimum_should_match=1
var shouldQueries []map[string]interface{}
for _, catID := range validCategories {
shouldQueries = append(shouldQueries, map[string]interface{}{
"prefix": map[string]interface{}{
"cat_id.kong_fu_zi_cat_id": catID,
},
})
}
if len(shouldQueries) > 0 {
// 将 should 条件包装成内层 bool 对象,作为 must 条件添加
innerBoolQuery := map[string]interface{}{
"bool": map[string]interface{}{
"should": shouldQueries,
"minimum_should_match": 1,
},
}
builder.AddBoolQuery("must", []map[string]interface{}{innerBoolQuery})
log.Printf("[DEBUG] Added kongfz category include query as nested bool in must (should match at least 1)")
}
}
}
// NewESQueryBuilder 创建 ES 查询构建器 // NewESQueryBuilder 创建 ES 查询构建器
func NewESQueryBuilder() *ESQueryBuilder { func NewESQueryBuilder() *ESQueryBuilder {
return &ESQueryBuilder{ return &ESQueryBuilder{