From fe34007bd8a3d6247c2035ff06a964b598fe1ae5 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 13 Mar 2026 13:49:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=89=E5=93=81=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/book.go | 237 +++++++ controller/pddCatId.go | 112 +-- es/es_search.go | 23 +- main.go | 37 +- model/request/book.go | 90 +++ model/response/book.go | 48 ++ service/book.go | 1471 ++++++++++++++++++++++++++++++++++++++++ util/common.go | 47 ++ 8 files changed, 1999 insertions(+), 66 deletions(-) create mode 100644 controller/book.go create mode 100644 model/request/book.go create mode 100644 model/response/book.go create mode 100644 service/book.go create mode 100644 util/common.go diff --git a/controller/book.go b/controller/book.go new file mode 100644 index 0000000..00d6c4e --- /dev/null +++ b/controller/book.go @@ -0,0 +1,237 @@ +package controller + +import ( + "centerBook/es" + "centerBook/model/request" + "centerBook/model/response" + "centerBook/service" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +// BookController 图书控制器 +type BookController struct { + bookService *service.BookService +} + +// NewBookController 创建图书控制器实例 +func NewBookController(bookService *service.BookService) *BookController { + return &BookController{ + bookService: bookService, + } +} + +// SearchBookBaseInfoHandler 搜索图书基础信息 Handler +func (b *BookController) SearchBookBaseInfoHandler(c *gin.Context) { + // 绑定请求参数 + var req request.BookSearchRequest + if err := c.ShouldBindQuery(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + // 设置默认值 + if req.Page <= 0 { + req.Page = 1 + } + if req.PageSize <= 0 { + if req.PerPage > 0 { + req.PageSize = req.PerPage + } else { + req.PageSize = 10 + } + } + + // 调用服务层查询 + list, total, err := b.bookService.SearchBookBaseInfo(&req) + if err != nil { + fmt.Printf("[ERROR] SearchBookBaseInfo failed: %v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 转换为响应格式 + responseList := make([]es.ESBookResponse, 0, len(list)) + for _, book := range list { + responseList = append(responseList, book.ConvertToResponse()) + } + + // DEBUG: 打印响应数据摘要 + fmt.Printf("[DEBUG] Response Info => total=%d page=%d pageSize=%d returnCount=%d\n", + total, req.Page, req.PageSize, len(responseList)) + + // 返回标准响应格式 + resp := response.NewBookSearchResponse(req.Page, req.PageSize, total, responseList) + c.JSON(http.StatusOK, resp) +} + +// AddBookToESHandler 根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新 Handler +func (b *BookController) AddBookToESHandler(c *gin.Context) { + var req es.ESBook + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "参数解析错误", + "message": err.Error(), + }) + return + } + + if req.ISBN == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "ISBN不能为空", + }) + return + } + // 调用服务层处理 + result, err := b.bookService.AddBookToESHandler(c.Request.Context(), &req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "处理失败", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "data": result.Book.ConvertToResponse(), + "source": result.Source, + }) +} + +// UpdateBookFieldsByISBNHandler 根据 ISBN 更新图书字段 Handler +func (b *BookController) UpdateBookFieldsByISBNHandler(c *gin.Context) { + // 绑定请求参数 + var req request.BookUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + "details": err.Error(), + }) + return + } + isbn := strings.TrimSpace(req.ISBN) + if isbn == "" { + c.JSON(400, gin.H{ + "error": "ISBN不能为空", + }) + return + } + + if len(req.Data) == 0 { + c.JSON(400, gin.H{ + "error": "至少提供一个要更新的字段", + }) + return + } + // 调用服务层处理 + result, err := b.bookService.UpdateBookFieldsByISBN(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "更新失败", + "details": err.Error(), + }) + return + } + + // 返回标准响应 + resp := response.NewUpdateBookResponse(result.ISBN, result.Updated, result.Fields) + c.JSON(http.StatusOK, resp) +} + +// UpdateBookCatIdByISBNHandler 根据 ISBN 更新图书字段 Handler +func (b *BookController) UpdateBookCatIdByISBNHandler(c *gin.Context) { + // 绑定请求参数 + var req request.BookUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + "details": err.Error(), + }) + return + } + isbn := strings.TrimSpace(req.ISBN) + if isbn == "" { + c.JSON(400, gin.H{ + "error": "ISBN不能为空", + }) + return + } + if len(req.Data) == 0 { + c.JSON(400, gin.H{ + "error": "至少提供一个要更新的字段", + }) + return + } + + // 调用服务层处理 + result, err := b.bookService.UpdateBookCatIdByISBNHandler(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "更新失败", + "details": err.Error(), + }) + return + } + + // 返回标准响应 + resp := response.NewUpdateBookResponse(result.ISBN, result.Updated, result.Fields) + c.JSON(http.StatusOK, resp) +} + +// DeleteBookHandler 删除图书 +func (b *BookController) DeleteBookHandler(c *gin.Context) { + // 绑定请求参数 + var req request.BookDelByIsbnRequest + if err := c.ShouldBindQuery(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + "details": err.Error(), + }) + return + } + // 调用服务层处理 + err := b.bookService.DeleteBookByISBN(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "删除失败", + "details": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) +} + +// DeleteBookByIDHandler 根据ID删除图书的HTTP处理器 +func (b *BookController) DeleteBookByIDHandler(c *gin.Context) { + // 绑定请求参数 + var req request.BookDelByIdRequest + if err := c.ShouldBindQuery(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "请求参数错误", + "details": err.Error(), + }) + return + } + + // 调用服务层处理 + err := b.bookService.DeleteBookByID(&req) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "删除失败", + "details": err.Error(), + }) + return + } + + c.JSON(200, gin.H{ + "code": 200, + "message": "删除成功", + "data": gin.H{ + "id": req.ID, + "deleted": true, + }, + }) +} diff --git a/controller/pddCatId.go b/controller/pddCatId.go index 86741bf..62a7db5 100644 --- a/controller/pddCatId.go +++ b/controller/pddCatId.go @@ -1,58 +1,58 @@ package controller -import ( - "centerBook/util/esClient" - "centerBook/util/pdd" - "centerBook/util/redisClient" - "context" - "log" -) - -// 推测pddCatId类目 -func getPddCatId() { - - ctx := context.Background() - // 为两个数据库创建命名客户端 - redisClient.AddClient("db4", "36.212.20.113", "j8nZ4jra2E", 4) - redisClient.AddClient("db14", "36.212.20.113", "j8nZ4jra2E", 14) - - // 从 db4 获取数据示例 - db4Client, err := redisClient.GetClientByName("db4") - if err == nil { - // 示例:获取键为 "key1" 的值 - val, err := db4Client.Get(ctx, "1995373681100910593").Result() - if err == nil { - log.Println(val) - } - } - - bookName := "" - - instance, err := pdd.GetPddInstance() - if err != nil { - return - } - instance.PddGoodsOuterCatMappingGet("", "15543", "书籍/杂志/报纸", "书籍 "+bookName) - - // 从 db14 获取数据示例 - db14Client, err := redisClient.GetClientByName("db14") - if err == nil { - // 示例:获取键为 "key2" 的值 - val, err := db14Client.Get(ctx, "key2").Result() - if err == nil { - // 处理获取到的值 - } - } - -} - -// connectES 连接到 Elasticsearch -func connectES(addresses []string, username, password string) (*esClient.ESClient, error) { - return esClient.NewESClient(addresses, username, password) -} - -// connectRedis 连接到 Redis -func connectRedis(addr, password string, db int) { - redisClient.InitRedis(addr, password, db) - redisClient.GetClient() -} +//import ( +// "centerBook/util/esClient" +// "centerBook/util/pdd" +// "centerBook/util/redisClient" +// "context" +// "log" +//) +// +//// 推测pddCatId类目 +//func getPddCatId() { +// +// ctx := context.Background() +// // 为两个数据库创建命名客户端 +// redisClient.AddClient("db4", "36.212.20.113", "j8nZ4jra2E", 4) +// redisClient.AddClient("db14", "36.212.20.113", "j8nZ4jra2E", 14) +// +// // 从 db4 获取数据示例 +// db4Client, err := redisClient.GetClientByName("db4") +// if err == nil { +// // 示例:获取键为 "key1" 的值 +// val, err := db4Client.Get(ctx, "1995373681100910593").Result() +// if err == nil { +// log.Println(val) +// } +// } +// +// bookName := "" +// +// instance, err := pdd.GetPddInstance() +// if err != nil { +// return +// } +// instance.PddGoodsOuterCatMappingGet("", "15543", "书籍/杂志/报纸", "书籍 "+bookName) +// +// // 从 db14 获取数据示例 +// db14Client, err := redisClient.GetClientByName("db14") +// if err == nil { +// // 示例:获取键为 "key2" 的值 +// val, err := db14Client.Get(ctx, "key2").Result() +// if err == nil { +// // 处理获取到的值 +// } +// } +// +//} +// +//// connectES 连接到 Elasticsearch +//func connectES(addresses []string, username, password string) (*esClient.ESClient, error) { +// return esClient.NewESClient(addresses, username, password) +//} +// +//// connectRedis 连接到 Redis +//func connectRedis(addr, password string, db int) { +// redisClient.InitRedis(addr, password, db) +// redisClient.GetClient() +//} diff --git a/es/es_search.go b/es/es_search.go index 647177c..cfc7421 100644 --- a/es/es_search.go +++ b/es/es_search.go @@ -5,6 +5,7 @@ import ( "bytes" "centerBook/image" "centerBook/kongfz" + "centerBook/model/request" "centerBook/tail" "centerBook/util/redisClient" "context" @@ -245,6 +246,10 @@ type ESBook struct { IsIllegal int `json:"is_illegal,omitempty"` // 是否非法 示例 000000 IsReturn int `json:"is_return,omitempty"` // 是否为驳回 示例 0 否 1 是 IsFilter string `json:"is_filter,omitempty"` // 过滤字段 + PageCount NumberOrString `json:"page_count"` // 页数 + WordCount NumberOrString `json:"word_count"` // 字数 + BookFormat NumberOrString `json:"book_format"` // 多少开 + CatId request.CatIdObject `json:"cat_id"` // 类目 } // AddBookRequest 用于 Service 方法的入参 @@ -1935,6 +1940,10 @@ func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, in if err != nil { return nil, 0, fmt.Errorf("复制响应数据失败: %v", err) } + err = writer.Flush() + if err != nil { + return nil, 0, fmt.Errorf("刷新缓冲区失败:%v", err) + } rawData := buf.Bytes() //rawData, err := io.ReadAll(res.Body) if err != nil { @@ -2575,6 +2584,7 @@ func (svc *ESSearchService) UpdateBookFieldsByISBNHandler(c *gin.Context) { // 先确认 ISBN 是否存在 book, err := svc.SearchBookByISBN(isbn) + fmt.Println("book:", book) if err != nil { c.JSON(500, gin.H{ "error": "查询ES失败", @@ -2641,7 +2651,7 @@ func (svc *ESSearchService) UpdateBookFieldsByISBN( params := make(map[string]interface{}) allowedFields := map[string]bool{ - "book_name": true, + //"book_name": true, "book_pic": true, "book_pic_s": true, "book_pic_b": true, @@ -2670,6 +2680,9 @@ func (svc *ESSearchService) UpdateBookFieldsByISBN( "is_return": true, "is_filter": true, "update_time": true, + "page_count": true, + "word_count": true, + "book_format": true, } for field, value := range data { @@ -2680,7 +2693,7 @@ func (svc *ESSearchService) UpdateBookFieldsByISBN( fmt.Sprintf("ctx._source.%s = params.%s;", field, field)) params[field] = value } - + fmt.Println("scriptParts:", scriptParts) if len(scriptParts) == 0 { return 0, fmt.Errorf("没有有效的字段可更新") } @@ -2697,7 +2710,6 @@ func (svc *ESSearchService) UpdateBookFieldsByISBN( }, }, } - payload, _ := json.Marshal(body) res, err := svc.ES.Client.UpdateByQuery( @@ -2711,6 +2723,11 @@ func (svc *ESSearchService) UpdateBookFieldsByISBN( } defer res.Body.Close() + if res.IsError() { + errorBody := res.String() + log.Printf("[UpdateBookFieldsByISBN] ES 返回错误 | status=%s | body=%s", res.Status(), errorBody) + return 0, fmt.Errorf("ES 返回错误:%s, 详细信息:%s", res.Status(), errorBody) + } if res.IsError() { return 0, fmt.Errorf("ES返回错误: %s", res.String()) } diff --git a/main.go b/main.go index 42a65e2..6870c43 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import ( "bytes" + "centerBook/controller" + "centerBook/service" "context" "crypto/md5" "database/sql" @@ -193,7 +195,9 @@ func main() { esService := es.NewESSearchService(esClient) - redisClient.AddClient("db4", "36.212.20.113:7963", "j8nZ4jra2E", 4) + redisClient.AddClient("db4", "36.212.12.247:6379", "long6166@@", 2) + redisClient.AddClient("db1", "36.212.12.247:6379", "long6166@@", 1) + //redisClient.AddClient("test", "127.0.0.1:6379", "", 0) // =================================================================== // 3. 初始化IP日志路径 @@ -293,13 +297,32 @@ func main() { // ISBN 模糊搜索 r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler) // ISBN 精确搜索 - r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) + r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler) //1 // 书名搜索 r.GET("/api/es/searchByBookName", esService.SearchBookByBookNameHandler) // 全字段搜索 r.GET("/api/es/searchAll", esService.SearchBooksAllFieldsHandler) // 根据条件查询 ES 图书信息 - r.GET("/api/es/getBookBaseInfoES", esService.SearchBookBaseInfoESHandler) + //r.GET("/api/es/getBookBaseInfoES", esService.SearchBookBaseInfoESHandler) //1 + + //------------------------------------------------------------------------ + // 初始化控制器 新 + bookSearchService := service.NewBookService(esClient) + bookController := controller.NewBookController(bookSearchService) + // 根据条件查询 ES 图书信息 + r.GET("/api/es/getBookBaseInfoES", bookController.SearchBookBaseInfoHandler) + // 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新 + //r.POST("/api/es/addBookToES", bookController.AddBookToESHandler) + // 更新:根据ISBN通用更新图书字段 + r.POST("/api/es/updateBookFieldsByISBN", bookController.UpdateBookFieldsByISBNHandler) + // 更新:根据ISBN通用更新图书字段 + r.POST("/api/es/updateBookCatIdByISBN", bookController.UpdateBookCatIdByISBNHandler) + // 删除:根据ISBN删除ES数据 + r.GET("/api/es/DeleteBookByISBN", bookController.DeleteBookHandler) + // 新增:根据ID删除ES数据 + r.GET("/api/es/DeleteBookByID", bookController.DeleteBookByIDHandler) + //------------------------------------------------------------------------ + // 新:核价软件用批量获取 r.GET("/api/es/batchGetBookBaseInfoES", esService.BatchGetBookBaseInfoESHandler) // 多条件高级搜索 @@ -313,9 +336,9 @@ func main() { // 新增:根据ISBN更新图书的is_suit字段 r.POST("/api/es/updateBookSuitByISBN", esService.UpdateBookSuitByISBNHandler) // 新增:根据ISBN通用更新图书字段 - r.POST("/api/es/updateBookFieldsByISBN", esService.UpdateBookFieldsByISBNHandler) + //r.POST("/api/es/updateBookFieldsByISBN", esService.UpdateBookFieldsByISBNHandler) //1 // 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新 - r.POST("/api/es/addBookToES", esService.AddBookToESHandler) + r.POST("/api/es/addBookToES", esService.AddBookToESHandler) //1 // 新增:完整插入接口,支持所有字段, r.POST("/api/es/addBookFullToES", esService.AddBookFullToESHandler) // 新增:批量插入接口,支持同时插入多本图书 @@ -324,9 +347,9 @@ func main() { r.GET("/api/es/checkBookExists", esService.CheckBookExistsByISBNHandler) r.POST("/api/es/checkBookExists", esService.CheckBookExistsByISBNHandler) // 删除:根据ISBN删除ES数据 - r.GET("/api/es/DeleteBookByISBN", esService.DeleteBookHandler) + //r.GET("/api/es/DeleteBookByISBN", esService.DeleteBookHandler) //1 // 新增:根据ID删除ES数据 - r.GET("/api/es/DeleteBookByID", esService.DeleteBookByIDHandler) + //r.GET("/api/es/DeleteBookByID", esService.DeleteBookByIDHandler) //1 // 新增:检查书名是否包含套装关键字 r.GET("/api/es/checkBookSuit", esService.CheckBookSuitHandler) diff --git a/model/request/book.go b/model/request/book.go new file mode 100644 index 0000000..1157038 --- /dev/null +++ b/model/request/book.go @@ -0,0 +1,90 @@ +package request + +// BookSearchRequest ES 搜索图书请求参数 +type BookSearchRequest struct { + Page int `form:"page"` // 页码 + PageSize int `form:"pageSize"` // 每页数量 + PerPage int `form:"per_page"` // 每页数量 (兼容字段) + SaleSelect string `form:"saleSelect"` // 销量筛选类型 + PicType string `form:"picType"` // 图片类型 + ShopType string `form:"shopType"` // 店铺类型 + BookName string `form:"book_name"` // 书名 + BookPic string `form:"book_pic"` // 图片筛选 + ISBN string `form:"isbn"` // ISBN + Author string `form:"author"` // 作者 + Category string `form:"category"` // 分类 + CategoryType string `form:"categoryType"` // ISBN分类类型 + Publisher string `form:"publisher"` // 出版社 + PublicationTime string `form:"publication_time"` // 出版时间 + BindingLayout string `form:"binding_layout"` // 装帧 + FixPrice string `form:"fix_price"` // 定价 + IsSuit string `form:"isSuit"` // 是否套装 + IsReturn string `form:"is_return"` // 是否驳回 + IsFilter string `form:"is_filter"` // 过滤字段 + BuyCounts string `form:"buy_counts"` // 购买次数 + SellCounts string `form:"sell_counts"` // 在售数量 + DaySale7 string `form:"day_sale_7"` // 7 天销量 + DaySale15 string `form:"day_sale_15"` // 15 天销量 + DaySale30 string `form:"day_sale_30"` // 30 天销量 + DaySale60 string `form:"day_sale_60"` // 60 天销量 + DaySale90 string `form:"day_sale_90"` // 90 天销量 + DaySale180 string `form:"day_sale_180"` // 180 天销量 + DaySale365 string `form:"day_sale_365"` // 365 天销量 + ThisYearSale string `form:"this_year_sale"` // 今年销量 + LastYearSale string `form:"last_year_sale"` // 去年销量 + TotalSaleRange string `form:"totalSale_range"` // 总销量范围 + ID string `form:"id"` // ID +} + +// BookUpdateRequest 更新图书请求参数 +type BookUpdateRequest struct { + ISBN string `json:"isbn" binding:"required"` + Data map[string]interface{} `json:"data" binding:"required"` +} + +// BookDelByIsbnRequest 更新图书请求参数 +type BookDelByIsbnRequest struct { + ISBN string `json:"isbn" form:"isbn" binding:"required"` +} + +// BookDelByIdRequest 更新图书请求参数 +type BookDelByIdRequest struct { + ID string `json:"id" form:"id" binding:"required"` +} + +// BookInfo 书籍信息结构 +type BookInfo 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"` // 分类 +} + +// ImageObject 图片对象结构 +type ImageObject struct { + CarouselUrlArray []string `json:"carousel_url_array"` // 轮播图 + WhiteBackgroundUrl string `json:"white_background_url"` // 白底图 + DetailUrlObject DetailImageObject `json:"detail_url_object"` // 详情对象 + DefaultImageUrl string `json:"default_image_url"` // 默认图 +} + +type CatIdObject struct { + PinDuoDuoCatId string `json:"pin_duo_duo_cat_id"` // 拼多多分类 ID + KongFuZiCatId string `json:"kong_fu_zi_cat_id"` // 孔夫子分类 ID + XianYuCatId string `json:"xian_yu_cat_id"` // 闲鱼分类 ID +} + +type DetailImageObject struct { + IntroductionUrl []string `json:"introduction_url"` // 简介图 + CatalogueUrl []string `json:"catalogue_url"` // 目录图 + LiveShootingUrl []string `json:"live_shooting_url"` // 实拍图 + OtherUrl []string `json:"other_url"` // 其他图 +} diff --git a/model/response/book.go b/model/response/book.go new file mode 100644 index 0000000..761fc3f --- /dev/null +++ b/model/response/book.go @@ -0,0 +1,48 @@ +package response + +import "centerBook/es" + +// BookSearchResponse 图书搜索响应 +type BookSearchResponse struct { + CurrentPage int `json:"current_page"` + Data []es.ESBookResponse `json:"data"` + PerPage int `json:"per_page"` + Total int `json:"total"` +} + +// NewBookSearchResponse 创建图书搜索响应 +func NewBookSearchResponse(page, pageSize, total int, data []es.ESBookResponse) *BookSearchResponse { + return &BookSearchResponse{ + CurrentPage: page, + Data: data, + PerPage: pageSize, + Total: total, + } +} + +// UpdateBookResponse 更新图书响应 +type UpdateBookResponse struct { + Code int `json:"code"` + Message string `json:"message"` + ISBN string `json:"isbn"` + Updated int `json:"updated"` + FieldsUpdated int `json:"fields_updated"` + UpdatedFields []string `json:"updated_fields"` +} + +// NewUpdateBookResponse 创建更新图书响应 +func NewUpdateBookResponse(isbn string, updated int, fields map[string]interface{}) *UpdateBookResponse { + fieldsList := make([]string, 0, len(fields)) + for k := range fields { + fieldsList = append(fieldsList, k) + } + + return &UpdateBookResponse{ + Code: 200, + Message: "success", + ISBN: isbn, + Updated: updated, + FieldsUpdated: len(fields), + UpdatedFields: fieldsList, + } +} diff --git a/service/book.go b/service/book.go new file mode 100644 index 0000000..ba1acbe --- /dev/null +++ b/service/book.go @@ -0,0 +1,1471 @@ +package service + +import ( + "bufio" + "bytes" + "centerBook/es" + "centerBook/model/request" + "centerBook/tail" + "centerBook/util" + "centerBook/util/redisClient" + "context" + "encoding/json" + "fmt" + "github.com/elastic/go-elasticsearch/v8/esapi" + jsoniter "github.com/json-iterator/go" + "io" + "log" + "strconv" + "strings" + "time" +) + +// BookService 图书搜索服务 +type BookService struct { + esClient *es.ESClient +} + +// NewBookService 创建图书搜索服务实例 +func NewBookService(esClient *es.ESClient) *BookService { + return &BookService{ + esClient: esClient, + } +} + +// UpdateBookResult 更新结果 +type UpdateBookResult struct { + ISBN string + Updated int + Fields map[string]interface{} +} + +// AddBookResult 添加图书结果 +type AddBookResult struct { + Book *es.ESBook + Source string // "es" 或 "service" +} + +// esHitsWrapper ES 命中包装器 +type esHitsWrapper struct { + Hits struct { + Total struct { + Value int `json:"value"` + } `json:"total"` + Hits []struct { + Index string `json:"_index"` + ID string `json:"_id"` + Source es.ESBook `json:"_source"` + } `json:"hits"` + } `json:"hits"` +} + +// QueryCondition 查询条件构建器 +type QueryCondition struct { + Field string // ES 字段名 + Value interface{} // 查询值 + Type string // 查询类型:term, match, prefix, range, wildcard, exists, bool + Operator string // match 操作符:and, or + GTE interface{} // range: 大于等于 + LTE interface{} // range: 小于等于 + Pattern string // wildcard: 匹配模式 + Must []map[string]interface{} `json:"must,omitempty"` // bool: must + MustNot []map[string]interface{} `json:"must_not,omitempty"` // bool: must_not + Should []map[string]interface{} `json:"should,omitempty"` // bool: should +} + +// SalesInfo 销量信息 +type SalesInfo struct { + DaySale7, DaySale15, DaySale30, DaySale60, DaySale90, DaySale180, DaySale365 int + ThisYearSale, LastYearSale, TotalSale int +} + +// ESQueryBuilder ES 查询构建器 +type ESQueryBuilder struct { + mustQueries []map[string]interface{} + boolMust []map[string]interface{} + boolMustNot []map[string]interface{} + boolShould []map[string]interface{} + minShouldMatch int +} + +// SearchBookBaseInfo 搜索图书基础信息 +func (svc *BookService) SearchBookBaseInfo(request *request.BookSearchRequest) ([]es.ESBook, int, error) { + queryBuilder := NewESQueryBuilder() + + // ===== saleSelect 对应字段映射 ===== + saleField := map[string]string{ + "7": "day_sale_7", + "15": "day_sale_15", + "30": "day_sale_30", + "60": "day_sale_60", + "90": "day_sale_90", + "180": "day_sale_180", + "365": "day_sale_365", + "0": "this_year_sale", + "1": "last_year_sale", + }[request.SaleSelect] + + log.Printf("[DEBUG] saleSelect=%s saleField=%s", request.SaleSelect, saleField) + + // saleField >= 1 默认条件 + if saleField != "" { + queryBuilder.AddQuery(&QueryCondition{ + Field: saleField, + Type: "range", + GTE: 1, + }) + } + + // ========== 构建查询条件 ========== + svc.buildISuitCondition(queryBuilder, request.IsSuit) + svc.buildIsReturnCondition(queryBuilder, request.IsReturn) + svc.buildIsFilterCondition(queryBuilder, request.IsFilter, request.ShopType) + svc.buildCategoryTypeCondition(queryBuilder, request.CategoryType) + svc.buildCategoryCondition(queryBuilder, request.Category) + svc.buildBookPicCondition(queryBuilder, request.BookPic, request.PicType) + svc.buildBuyCountsCondition(queryBuilder, request.BuyCounts, saleField) + svc.buildTotalSaleRangeCondition(queryBuilder, request.TotalSaleRange) + svc.buildNumericRangeConditions(queryBuilder, request, saleField) + svc.buildExactMatchConditions(queryBuilder, request) + svc.buildFuzzyMatchConditions(queryBuilder, request) + svc.buildDefaultPrefixConditions(queryBuilder, request) + + // ========== 分页和排序 ========== + from := (request.Page - 1) * request.PageSize + log.Printf("[DEBUG] page=%d pageSize=%d from=%d", request.Page, request.PageSize, from) + + var sort []map[string]interface{} + if request.PageSize >= 500 { + sort = []map[string]interface{}{ + {"id": map[string]interface{}{"order": "asc"}}, + } + } else { + sort = []map[string]interface{}{ + {"update_time": map[string]interface{}{"order": "desc"}}, + } + } + + // ========== 构建 ES 查询 ========== + query := queryBuilder.Build(from, request.PageSize, sort) + + body, _ := json.MarshalIndent(query, "", " ") + log.Printf("[DEBUG] ES Query Body:\n%s", string(body)) + + // ========== 执行 ES 查询 ========== + res, err := svc.esClient.Client.Search( + svc.esClient.Client.Search.WithIndex(es.ESIndex), + svc.esClient.Client.Search.WithBody(bytes.NewReader(body)), + svc.esClient.Client.Search.WithTrackTotalHits(true), + ) + + log.Printf("[DEBUG] ES Query Response Status: %s", res.Status) + + if err != nil { + log.Printf("[ERROR] ES.Client.Search error: %v", err) + return nil, 0, err + } + defer res.Body.Close() + + // 读取响应 + var buf bytes.Buffer + // 使用CopyBuffer可以重用缓冲区 + writer := bufio.NewWriterSize(&buf, 8192) + _, err = io.Copy(writer, res.Body) + if err != nil { + return nil, 0, fmt.Errorf("复制响应数据失败: %v", err) + } + err = writer.Flush() + if err != nil { + return nil, 0, fmt.Errorf("刷新缓冲区失败:%v", err) + } + + rawData := buf.Bytes() + if err != nil { + return nil, 0, fmt.Errorf("读取响应失败: %v", err) + } + // 检查是否有数据 + if len(rawData) == 0 { + return nil, 0, fmt.Errorf("ES返回空响应") + } + + // 验证是否是有效的JSON + if rawData[0] != '{' { + return nil, 0, fmt.Errorf("ES返回非JSON响应: %s", string(rawData[:min(100, len(rawData))])) + } + + fmt.Println("[DEBUG] ES query executed successfully") + + var _json = jsoniter.ConfigCompatibleWithStandardLibrary + var parsed esHitsWrapper + if err := _json.Unmarshal(rawData, &parsed); err != nil { + return nil, 0, fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, string(rawData[:min(200, len(rawData))])) + } + list := make([]es.ESBook, 0, len(parsed.Hits.Hits)) + for _, hit := range parsed.Hits.Hits { + list = append(list, hit.Source) + } + + return list, parsed.Hits.Total.Value, nil +} + +// AddBookToESHandler 根据 ISBN 查询 ES 中是否存在,不存在则新增,存在则更新 +func (svc *BookService) AddBookToESHandler(ctx context.Context, req *es.ESBook) (*AddBookResult, error) { + // 先查 ES 是否存在 + book, err := svc.SearchBookByISBN(req.ISBN) + if err != nil { + return nil, fmt.Errorf("查询 ES 失败:%v", err) + } + // 已存在,处理更新逻辑 + if book != nil { + updateData := svc.buildUpdateData(book, req) + if len(updateData) > 0 { + updateData["update_time"] = fmt.Sprintf("%d", time.Now().Unix()) + if _, err := svc.UpdateBookFieldsByISBN(&request.BookUpdateRequest{ + ISBN: req.ISBN, + Data: updateData, + }); err != nil { + return nil, fmt.Errorf("补全 ES 字段失败:%v", err) + } + + // 重新查询最新数据 + book, _ = svc.SearchBookByISBN(req.ISBN) + } + + return &AddBookResult{ + Book: book, + Source: "es", + }, nil + } + + // 不存在,执行新增 + newBook, err := svc.addBookToES(ctx, req) + if err != nil { + return &AddBookResult{ + Book: nil, + Source: "", + }, nil + } + return &AddBookResult{ + Book: newBook, + Source: "service", + }, nil +} + +// UpdateBookFieldsByISBN 更新图书字段 +func (svc *BookService) UpdateBookFieldsByISBN(request *request.BookUpdateRequest) (*UpdateBookResult, error) { + // 先确认 ISBN 是否存在 + book, err := svc.SearchBookByISBN(request.ISBN) + if err != nil { + return nil, fmt.Errorf("查询 ES 失败:%v", err) + } + + if book == nil { + return nil, fmt.Errorf("未找到该 ISBN 对应的图书") + } + // 构建更新脚本 + 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,自动检测并设置 + params["is_suit"] = map[bool]int{true: 1, false: 0}[es.CheckBookSuit(book.BookName.Value)] + scriptParts = append(scriptParts, fmt.Sprintf("ctx._source.is_suit = params.is_suit;")) + } + + for field, value := range request.Data { + if !allowedFields[field] { + continue + } + scriptParts = append(scriptParts, + fmt.Sprintf("ctx._source.%s = params.%s;", field, field)) + params[field] = value + } + if len(scriptParts) == 0 { + return nil, fmt.Errorf("没有有效的更新字段") + } + + body := map[string]interface{}{ + "script": map[string]interface{}{ + "source": strings.Join(scriptParts, " "), + "lang": "painless", + "params": params, + }, + "query": map[string]interface{}{ + "term": map[string]interface{}{ + "isbn": request.ISBN, + }, + }, + } + payload, _ := json.Marshal(body) + + res, err := svc.esClient.Client.UpdateByQuery( + []string{es.ESIndex}, + svc.esClient.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)), + svc.esClient.Client.UpdateByQuery.WithRefresh(true), + svc.esClient.Client.UpdateByQuery.WithConflicts("proceed"), + ) + if err != nil { + return nil, fmt.Errorf("ES更新失败: %s", err) + } + defer res.Body.Close() + + if res.IsError() { + return nil, fmt.Errorf("ES返回错误: %s", res.String()) + } + + // 解析响应 + var parsed struct { + Updated int `json:"updated"` + } + + if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { + return nil, fmt.Errorf("解析 ES 响应失败:%v", err) + } + + log.Printf("[INFO] UpdateBookFieldsByISBN | ISBN=%s | updated=%d", + request.ISBN, parsed.Updated) + + // 同步 Redis + _ = svc.SyncRedisByISBN(request.ISBN, "update") + + return &UpdateBookResult{ + ISBN: request.ISBN, + Updated: parsed.Updated, + Fields: request.Data, + }, nil +} + +// UpdateBookCatIdByISBNHandler 更新图书字段 +func (svc *BookService) UpdateBookCatIdByISBNHandler(request *request.BookUpdateRequest) (*UpdateBookResult, error) { + // 先确认 ISBN 是否存在 + book, err := svc.SearchBookByISBN(request.ISBN) + if err != nil { + return nil, fmt.Errorf("查询 ES 失败:%v", err) + } + + if book == nil { + return nil, fmt.Errorf("未找到该 ISBN 对应的图书") + } + catIdValue, exists := request.Data["cat_id"] + if !exists { + return nil, fmt.Errorf("没有有效的更新字段") + } + + body := map[string]interface{}{ + "script": map[string]interface{}{ + "source": "ctx._source.cat_id = params.cat_id;", + "lang": "painless", + "params": map[string]interface{}{ + "cat_id": catIdValue, + }, + }, + "query": map[string]interface{}{ + "term": map[string]interface{}{ + "isbn": request.ISBN, + }, + }, + } + payload, _ := json.Marshal(body) + + res, err := svc.esClient.Client.UpdateByQuery( + []string{es.ESIndex}, + svc.esClient.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)), + svc.esClient.Client.UpdateByQuery.WithRefresh(true), + svc.esClient.Client.UpdateByQuery.WithConflicts("proceed"), + ) + if err != nil { + return nil, fmt.Errorf("ES更新失败: %s", err) + } + defer res.Body.Close() + + if res.IsError() { + return nil, fmt.Errorf("ES返回错误: %s", res.String()) + } + + // 解析响应 + var parsed struct { + Updated int `json:"updated"` + } + + if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { + return nil, fmt.Errorf("解析 ES 响应失败:%v", err) + } + + log.Printf("[INFO] UpdateBookFieldsByISBN | ISBN=%s | updated=%d", + request.ISBN, parsed.Updated) + + // 同步 Redis + _ = svc.SyncRedisByISBN(request.ISBN, "update") + + return &UpdateBookResult{ + ISBN: request.ISBN, + Updated: parsed.Updated, + Fields: map[string]interface{}{"cat_id": catIdValue}, + }, nil +} + +// DeleteBookByISBN 删除图书 +func (svc *BookService) DeleteBookByISBN(request *request.BookDelByIsbnRequest) error { + isbn := strings.TrimSpace(request.ISBN) + log.Printf("[DeleteBookByISBN] 开始删除 | ISBN=%s", isbn) + + if isbn == "" { + return fmt.Errorf("ISBN 不能为空") + } + + query := fmt.Sprintf(`{ + "query": { + "term": { + "isbn": "%s" + } + } + }`, isbn) + + res, err := svc.esClient.Client.DeleteByQuery( + []string{es.ESIndex}, + strings.NewReader(query), + svc.esClient.Client.DeleteByQuery.WithRefresh(true), + ) + if err != nil { + return fmt.Errorf("执行 DeleteByQuery 失败: %v", err) + } + defer res.Body.Close() + + if res.IsError() { + return fmt.Errorf("ES 返回错误: %s", res.String()) + } + + // 同步 Redis + _ = svc.SyncRedisByISBN(request.ISBN, "del") + log.Printf("[DeleteBookByISBN] 成功删除 ISBN=%s 对应的文档", isbn) + return nil +} + +// DeleteBookByID 通过 ID 删除 ES 文档 +func (svc *BookService) DeleteBookByID(request *request.BookDelByIdRequest) error { + id := strings.TrimSpace(request.ID) + log.Printf("[DeleteBookByID] 开始删除 | ID=%s", id) + if id == "" { + return fmt.Errorf("ID 不能为空") + } + isbn, err := svc.SearchBookISBNByID(id) + if err != nil { + log.Printf("[DeleteBookByID] 获取 ISBN 失败:%v", err) + } + + query := fmt.Sprintf(`{ + "query": { + "term": { + "id": "%s" + } + } + }`, id) + + res, err := svc.esClient.Client.DeleteByQuery( + []string{es.ESIndex}, + strings.NewReader(query), + svc.esClient.Client.DeleteByQuery.WithRefresh(true), + ) + if err != nil { + return fmt.Errorf("执行 DeleteByQuery 失败: %v", err) + } + defer res.Body.Close() + + if res.IsError() { + return fmt.Errorf("ES 返回错误: %s", res.String()) + } + + // 同步 Redis + if isbn != "" { + _ = svc.SyncRedisByISBN(isbn, "del") + } + log.Printf("[DeleteBookByID] 成功删除 ID=%s 对应的文档", id) + return nil +} + +// SearchBookISBNByID 根据 ES ID 查询 ISBN +func (svc *BookService) SearchBookISBNByID(id string) (string, error) { + log.Printf("[SearchBookISBNByID] 开始查询 | ID=%s", id) + + query := map[string]interface{}{ + "query": map[string]interface{}{ + "term": map[string]interface{}{ + "id": id, + }, + }, + "_source": []string{"isbn"}, + "size": 1, + } + + body, err := json.Marshal(query) + if err != nil { + log.Printf("[SearchBookISBNByID] 构建查询 JSON 失败:%v", err) + return "", fmt.Errorf("构建查询 JSON 失败:%v", err) + } + + res, err := svc.esClient.Client.Search( + svc.esClient.Client.Search.WithIndex(es.ESIndex), + svc.esClient.Client.Search.WithBody(bytes.NewReader(body)), + svc.esClient.Client.Search.WithTrackTotalHits(true), + ) + if err != nil { + log.Printf("[SearchBookISBNByID] ES 查询失败:%v", err) + return "", fmt.Errorf("ES 查询失败:%v", err) + } + defer res.Body.Close() + + if res.IsError() { + log.Printf("[SearchBookISBNByID] ES 返回错误:%s", res.String()) + return "", fmt.Errorf("ES 返回错误:%s", res.String()) + } + + var parsed esHitsWrapper + + if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { + log.Printf("[SearchBookISBNByID] 解析 ES 响应失败:%v", err) + return "", fmt.Errorf("解析 ES 响应失败:%v", err) + } + + if len(parsed.Hits.Hits) == 0 { + log.Printf("[SearchBookISBNByID] 未找到 ID=%s 对应文档", id) + return "", nil + } + + isbn := parsed.Hits.Hits[0].Source.ISBN + log.Printf("[SearchBookISBNByID] 查询到 ISBN: %s", isbn) + + return isbn, nil +} + +// buildUpdateData 构建更新数据(只补充空值字段) +func (svc *BookService) buildUpdateData(existing, new *es.ESBook) map[string]interface{} { + updateData := make(map[string]interface{}) + + // 定义字段检查规则 + fieldChecks := []struct { + condition bool + key string + value interface{} + }{ + {existing.Publisher == "" && new.Publisher != "", "publisher", new.Publisher}, + {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", + map[string]interface{}{"localPath": "", "pddPath": new.BookPic.PddPath}}, + {existing.BookPicS.PddResponse == "" && new.BookPicS.PddResponse != "", "book_pic_s", + map[string]interface{}{"localPath": "", "pddResponse": new.BookPicS.PddResponse}}, + } + + // 遍历并添加需要更新的字段 + for _, check := range fieldChecks { + if check.condition { + updateData[check.key] = check.value + } + } + + log.Printf("更新数据:%+v", updateData) + return updateData +} + +// addBookToES 新增图书到 ES +func (svc *BookService) addBookToES(ctx context.Context, req *es.ESBook) (*es.ESBook, error) { + if req.ISBN == "" { + return nil, fmt.Errorf("ISBN 不能为空") + } + + // 获取并解析销量数据 + salesData, _ := tail.CheckSales([]string{req.ISBN}) + salesInfo := svc.parseSalesData(salesData, req.BuyCounts) + + // 处理出版时间,转换为时间戳 + publicationTimeTimestamp := req.PublicationTime + if req.PublicationTime != "" && req.PublicationTime != "0" { + publicationTimeTimestamp = strconv.FormatInt(util.ParsePublicationTime(publicationTimeTimestamp), 10) + } + // 构建 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, + } + // 写入 ES + if err := svc.indexDocumentToES(ctx, doc, req.ISBN); err != nil { + return nil, err + } + + // 同步 Redis + _ = svc.SyncRedisByISBN(req.ISBN, "update") + + // 构建返回对象 + return &es.ESBook{ + ID: doc["id"].(int64), + BookName: es.FlexibleString{Value: req.BookName.Value}, + BookPic: doc["book_pic"].(es.BookPicObj), + BookPicS: doc["book_pic_s"].(es.BookPicSObj), + BookPicB: req.BookPicB, + BookPicW: doc["book_pic_w"].(map[string]interface{}), + ISBN: req.ISBN, + Author: req.Author, + Category: req.Category, + Publisher: req.Publisher, + PublicationTime: req.PublicationTime, + BindingLayout: req.BindingLayout, + FixPrice: req.FixPrice, + Content: req.Content, + IsSuit: doc["is_suit"].(int), + DaySale7: doc["day_sale_7"].(int), + DaySale15: doc["day_sale_15"].(int), + DaySale30: doc["day_sale_30"].(int), + DaySale60: doc["day_sale_60"].(int), + DaySale90: doc["day_sale_90"].(int), + DaySale180: doc["day_sale_180"].(int), + DaySale365: doc["day_sale_365"].(int), + ThisYearSale: doc["this_year_sale"].(int), + LastYearSale: doc["last_year_sale"].(int), + TotalSale: doc["total_sale"].(int), + BuyCounts: req.BuyCounts, + SellCounts: req.SellCounts, + BookPicObj: req.BookPicObj, + BookPicObjS: req.BookPicObjS, + UpdateTime: doc["update_time"].(es.NumberOrString), + IsIllegal: 0, + IsReturn: 0, + IsFilter: "000000", + PageCount: req.PageCount, + WordCount: req.WordCount, + BookFormat: req.BookFormat, + }, nil +} + +// SyncRedisByISBN 同步到Redis +func (svc *BookService) SyncRedisByISBN(isbn string, act string) error { + client, err := redisClient.GetClientByName("db1") + if err != nil { + log.Printf("[SyncRedisByISBN] 获取 Redis 客户端失败:%v", err) + return err + } + if act == "del" { + exists, _ := client.Exists(context.Background(), isbn).Result() + fmt.Println("[SyncRedisByISBN] exists:", exists) + if exists > 0 { + if err := client.Del(context.Background(), isbn).Err(); err != nil { + log.Printf("[SyncRedisByISBN] 删除 Redis 失败:%v", err) + return err + } + } + } else { + book, err := svc.SearchBookByISBN(isbn) + if err != nil { + log.Printf("[SyncRedisByISBN] 查询 ES 错误:%v", err) + return err + } + redisBookInfo := request.BookInfo{} + + if book.ISBN != "" { + redisBookInfo.Isbn = book.ISBN + } + if book.BookName.Value != "" { + redisBookInfo.BookName = book.BookName.Value + } + if book.Author != "" { + redisBookInfo.Author = book.Author + } + if book.Publisher != "" { + redisBookInfo.Publishing = book.Publisher + } + if book.PublicationTime != "" && book.PublicationTime != "0" { + publicationTimeIn64, err := strconv.ParseInt(book.PublicationTime, 10, 64) + if err == nil { + redisBookInfo.PublicationDate = time.Unix(publicationTimeIn64, 0).Format("2006-01") + } + } + if book.BindingLayout != "" { + redisBookInfo.Binding = book.BindingLayout + } + if book.PageCount != "" && book.PageCount != "0" { + pageCount, err := strconv.ParseInt(string(book.PageCount), 10, 64) + if err == nil { + redisBookInfo.PagesCount = pageCount + } + } + if book.WordCount != "" && book.WordCount != "0" { + wordCount, err := strconv.ParseInt(string(book.WordCount), 10, 64) + if err == nil { + redisBookInfo.WordsCount = wordCount + } + } + if book.BookFormat != "" && book.BookFormat != "0" { + bookFormat, err := strconv.ParseInt(string(book.BookFormat), 10, 64) + if err == nil { + redisBookInfo.Format = bookFormat + } + } + + whiteBackgroundUrl := book.BookPicB + carouselUrls := []string{} + liveShootingUrl := []string{} + if book.BookPic.PddPath != "" { + carouselUrls = append(carouselUrls, book.BookPic.PddPath) + } + if book.BookPicS.PddResponse != "" { + liveShootingUrl = append(liveShootingUrl, book.BookPicS.PddResponse) + } + redisBookInfo.ImageObject = &request.ImageObject{ + CarouselUrlArray: carouselUrls, + WhiteBackgroundUrl: whiteBackgroundUrl, + DefaultImageUrl: book.BookDefPic.PddPath, + DetailUrlObject: request.DetailImageObject{ + IntroductionUrl: []string{}, + CatalogueUrl: []string{}, + LiveShootingUrl: liveShootingUrl, + OtherUrl: []string{}, + }, + } + + redisBookInfo.Price = int64(book.FixPrice) + + redisBookInfo.CatIdObject = request.CatIdObject{ + PinDuoDuoCatId: book.CatId.PinDuoDuoCatId, + KongFuZiCatId: book.CatId.KongFuZiCatId, + XianYuCatId: book.CatId.XianYuCatId, + } + + jsonData, err := json.Marshal(redisBookInfo) + if err != nil { + log.Printf("[SyncRedisByISBN] 序列化 BookInfo 失败:%v", err) + return err + } + + if err := client.Set(context.Background(), isbn, jsonData, 0).Err(); err != nil { + log.Printf("[SyncRedisByISBN] 更新 Redis 失败:%v", err) + return err + } + } + + log.Printf("[SyncRedisByISBN] 成功同步 ISBN=%s 到 Redis", isbn) + return nil +} + +// generateNewID 生成新 ID +func (svc *BookService) generateNewID() int64 { + lastID, err := svc.GetLastID() + if err != nil { + log.Printf("[WARN] GetLastID failed: %v, using timestamp", err) + return time.Now().UnixNano() / 1e6 + } + return int64(lastID + 1) +} + +// parseSalesData 解析销量数据 +func (svc *BookService) parseSalesData(salesData *tail.SalesResponse, buyCounts int64) *SalesInfo { + info := &SalesInfo{} + if salesData == nil || salesData.Data == nil { + return info + } + + parse := func(s string) int { + if v, _ := strconv.Atoi(s); v > 0 { + return v + } + return 0 + } + + for _, s := range salesData.Data { + info = &SalesInfo{ + DaySale7: parse(s.DaySale7), DaySale15: parse(s.DaySale15), + DaySale30: parse(s.DaySale30), DaySale60: parse(s.DaySale60), + DaySale90: parse(s.DaySale90), DaySale180: parse(s.DaySale180), + DaySale365: parse(s.DaySale365), ThisYearSale: parse(s.ThisYearSale), + LastYearSale: parse(s.LastYearSale), TotalSale: parse(s.Sale), + } + break + } + + if info.TotalSale == 0 && buyCounts > 0 { + info.TotalSale = int(buyCounts) + } + return info +} + +// buildBookMapForSerialization 构建序列化用的 map +func (svc *BookService) buildBookMapForSerialization(book *es.ESBook) map[string]interface{} { + return map[string]interface{}{ + "id": book.ID, + "book_name": book.BookName.Value, + "book_pic": book.BookPic, + "book_pic_s": book.BookPicS, + "book_pic_b": book.BookPicB, + "book_pic_w": book.BookPicW, + "isbn": book.ISBN, + "author": book.Author, + "category": book.Category, + "publisher": book.Publisher, + "publication_time": book.PublicationTime, + "binding_layout": book.BindingLayout, + "fix_price": book.FixPrice, + "content": book.Content, + "is_suit": book.IsSuit, + "day_sale_7": book.DaySale7, + "day_sale_15": book.DaySale15, + "day_sale_30": book.DaySale30, + "day_sale_60": book.DaySale60, + "day_sale_90": book.DaySale90, + "day_sale_180": book.DaySale180, + "day_sale_365": book.DaySale365, + "this_year_sale": book.ThisYearSale, + "last_year_sale": book.LastYearSale, + "total_sale": book.TotalSale, + "buy_counts": book.BuyCounts, + "sell_counts": book.SellCounts, + "book_pic_obj": book.BookPicObj, + "book_pic_obj_s": book.BookPicObjS, + "is_illegal": book.IsIllegal, + "is_return": book.IsReturn, + "is_filter": book.IsFilter, + "update_time": book.UpdateTime, + } +} + +// indexDocumentToES 写入 ES +func (svc *BookService) indexDocumentToES(ctx context.Context, doc map[string]interface{}, id string) error { + jsonData, _ := json.Marshal(doc) + + esReq := esapi.IndexRequest{ + Index: es.ESIndex, + DocumentID: id, + Body: bytes.NewReader(jsonData), + Refresh: "true", + } + + res, err := esReq.Do(ctx, svc.esClient.Client.Transport) + if err != nil { + return fmt.Errorf("ES 写入失败:%w", err) + } + defer res.Body.Close() + + if res.IsError() { + return fmt.Errorf("ES 错误:%s", res.String()) + } + + log.Printf("[AddBookToES] 成功 | ISBN=%s", id) + return nil +} + +// GetLastID 获取最后一条 ID +func (svc *BookService) GetLastID() (int, error) { + query := `{ + "size": 1, + "sort": [{"id": {"order": "desc"}}] + }` + + res, err := svc.esClient.Client.Search( + svc.esClient.Client.Search.WithContext(context.Background()), + svc.esClient.Client.Search.WithIndex(es.ESIndex), + svc.esClient.Client.Search.WithBody(strings.NewReader(query)), + ) + if err != nil { + log.Printf("[GetLastID] ES 查询失败: %v\n", err) + return 0, fmt.Errorf("ES 查询失败: %w", err) + } + defer res.Body.Close() + + if res.IsError() { + log.Printf("[GetLastID] ES 返回错误: %s\n", res.String()) + return 0, fmt.Errorf("ES 返回错误: %s", res.String()) + } + // 定义结构体,ID 为数组 + var result struct { + Hits struct { + Hits []struct { + Source struct { + ID int `json:"id"` // 改回单个 int + } `json:"_source"` + } `json:"hits"` + } `json:"hits"` + } + + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + log.Printf("[GetLastID] 解析 ES 返回 JSON 失败: %v\n", err) + return 0, fmt.Errorf("解析 ES 返回 JSON 失败: %w", err) + } + + if len(result.Hits.Hits) == 0 { + log.Println("[GetLastID] 没有找到任何文档") + return 0, nil + } + lastID := result.Hits.Hits[0].Source.ID + log.Printf("[GetLastID] 查询到最新文档 ID: %d\n", lastID) + + return lastID, nil +} + +// SearchBookByISBN 根据ISBN查询图书(在当前类中封装) +func (svc *BookService) SearchBookByISBN(isbn string) (*es.ESBook, error) { + log.Printf("[SearchBookByISBN] 开始查询 | ISBN=%s", isbn) + + 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("[SearchBookByISBN] 构建查询 JSON 失败:%v", err) + return nil, fmt.Errorf("构建查询 JSON 失败:%v", err) + } + + res, err := svc.esClient.Client.Search( + svc.esClient.Client.Search.WithIndex(es.ESIndex), + svc.esClient.Client.Search.WithBody(bytes.NewReader(body)), + svc.esClient.Client.Search.WithTrackTotalHits(true), + ) + if err != nil { + log.Printf("[SearchBookByISBN] ES 查询失败:%v", err) + return nil, fmt.Errorf("ES 查询失败:%v", err) + } + defer res.Body.Close() + + if res.IsError() { + log.Printf("[SearchBookByISBN] 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("[SearchBookByISBN] 解析 ES 响应失败:%v", err) + return nil, fmt.Errorf("解析 ES 响应失败:%v", err) + } + + if len(parsed.Hits.Hits) == 0 { + log.Printf("[SearchBookByISBN] 未找到 ISBN=%s 对应文档", isbn) + return nil, nil + } + + book := parsed.Hits.Hits[0].Source + log.Printf("[SearchBookByISBN] 查询到文档: %+v", book) + + return &book, nil +} + +// buildISuitCondition 构建 is_suit 查询条件 +func (svc *BookService) buildISuitCondition(builder *ESQueryBuilder, isSuit string) { + if isSuit == "" { + return + } + log.Printf("[DEBUG] is_suit val=%q", isSuit) + if num, err := strconv.Atoi(isSuit); err == nil { + builder.AddQuery(&QueryCondition{ + Field: "is_suit", + Value: num, + Type: "term", + }) + } else { + log.Printf("[ERROR] is_suit Atoi error: %v", err) + } +} + +// buildIsReturnCondition 构建 is_return 查询条件 +func (svc *BookService) buildIsReturnCondition(builder *ESQueryBuilder, isReturn string) { + if isReturn == "" { + return + } + log.Printf("[DEBUG] is_return val=%q", isReturn) + if num, err := strconv.Atoi(isReturn); err == nil { + builder.AddQuery(&QueryCondition{ + Field: "is_return", + Value: num, + Type: "term", + }) + } else { + log.Printf("[ERROR] is_return Atoi error: %v", err) + } +} + +// buildIsFilterCondition 构建 is_filter 查询条件 +func (svc *BookService) buildIsFilterCondition(builder *ESQueryBuilder, isFilter, shopType string) { + if isFilter != "1" && isFilter != "2" { + return + } + log.Printf("[DEBUG] is_filter val=%q", isFilter) + + targetBit := "1" + if isFilter == "2" { + targetBit = "0" + } + + var pattern string + switch shopType { + case "0": + pattern = targetBit + "*" + case "1": + pattern = "?" + targetBit + "*" + case "2": + pattern = "??" + targetBit + "*" + case "3": + pattern = "???" + targetBit + "*" + default: + return + } + + builder.AddQuery(&QueryCondition{ + Field: "is_filter", + Type: "wildcard", + Pattern: pattern, + }) +} + +// buildCategoryTypeCondition 构建 categoryType 查询条件 +func (svc *BookService) buildCategoryTypeCondition(builder *ESQueryBuilder, categoryType string) { + if categoryType == "" { + return + } + + if categoryType == "1" { + builder.AddQuery(&QueryCondition{ + Field: "isbn", + Value: "9787", + Type: "prefix", + }) + } else { + builder.AddBoolQuery("must_not", []map[string]interface{}{ + {"prefix": map[string]interface{}{"isbn": "9787"}}, + }) + } +} + +// buildCategoryCondition 构建 category 查询条件 +func (svc *BookService) buildCategoryCondition(builder *ESQueryBuilder, category string) { + if category == "" { + return + } + + if category == "排除大学教材" { + builder.AddBoolQuery("must_not", []map[string]interface{}{ + {"match_phrase": map[string]interface{}{ + "category": "图书/教材教辅考试/大学教材", + }}, + }) + } else { + builder.AddQuery(&QueryCondition{ + Field: "category", + Value: category, + Type: "match_phrase", + }) + } +} + +// buildBookPicCondition 构建 book_pic 查询条件 +func (svc *BookService) buildBookPicCondition(builder *ESQueryBuilder, bookPic, picType string) { + if bookPic == "" { + return + } + + targetField := "book_pic.pddPath" + if picType != "1" && picType != "" { + targetField = "book_pic_s.pddResponse" + } + + if bookPic == "1" { + // 有图:字段必须存在且非空 + builder.AddBoolQuery("must", []map[string]interface{}{ + {"exists": map[string]interface{}{"field": targetField}}, + {"wildcard": map[string]interface{}{targetField: "*"}}, + }) + } else { + // 无图:字段不存在或为空 + keywordField := "book_pic.pddPath.keyword" + if picType != "1" && picType != "" { + keywordField = "book_pic_s.pddResponse.keyword" + } + + builder.AddBoolQuery("should", []map[string]interface{}{ + { + "bool": map[string]interface{}{ + "must_not": []map[string]interface{}{ + {"exists": map[string]interface{}{"field": keywordField}}, + }, + }, + }, + { + "term": map[string]interface{}{keywordField: ""}, + }, + }, 1) // minimum_should_match = 1 + } +} + +// buildBuyCountsCondition 构建 buy_counts 查询条件 +func (svc *BookService) buildBuyCountsCondition(builder *ESQueryBuilder, buyCounts, saleField string) { + if buyCounts == "" || saleField == "" { + return + } + log.Printf("[DEBUG] buy_counts uses saleField=%s", saleField) + + parts := strings.Split(buyCounts, ",") + if len(parts) == 2 { + minVal, _ := strconv.Atoi(parts[0]) + maxVal, _ := strconv.Atoi(parts[1]) + builder.AddQuery(&QueryCondition{ + Field: saleField, + Type: "range", + GTE: minVal, + LTE: maxVal, + }) + return + } + + if num, err := strconv.Atoi(buyCounts); err == nil { + builder.AddQuery(&QueryCondition{ + Field: saleField, + Type: "range", + GTE: num, + }) + } +} + +// buildTotalSaleRangeCondition 构建 totalSale_range 查询条件 +func (svc *BookService) buildTotalSaleRangeCondition(builder *ESQueryBuilder, totalSaleRange string) { + if totalSaleRange == "" { + return + } + parts := strings.Split(totalSaleRange, ",") + if len(parts) == 2 { + minVal, _ := strconv.Atoi(parts[0]) + maxVal, _ := strconv.Atoi(parts[1]) + builder.AddQuery(&QueryCondition{ + Field: "total_sale", + Type: "range", + GTE: minVal, + LTE: maxVal, + }) + } +} + +// buildNumericRangeConditions 构建数值范围查询条件 +func (svc *BookService) buildNumericRangeConditions(builder *ESQueryBuilder, request *request.BookSearchRequest, saleField string) { + fields := map[string]string{ + "SellCounts": request.SellCounts, + "DaySale7": request.DaySale7, + "DaySale15": request.DaySale15, + "DaySale30": request.DaySale30, + "DaySale60": request.DaySale60, + "DaySale90": request.DaySale90, + "DaySale180": request.DaySale180, + "DaySale365": request.DaySale365, + "ThisYearSale": request.ThisYearSale, + "LastYearSale": request.LastYearSale, + "PublicationTime": request.PublicationTime, + } + + esFields := map[string]string{ + "SellCounts": "sell_counts", + "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", + "PublicationTime": "publication_time", + } + + for fieldName, value := range fields { + if value == "" { + continue + } + esField := esFields[fieldName] + parts := strings.Split(value, ",") + if len(parts) == 2 { + minVal, _ := strconv.Atoi(parts[0]) + maxVal, _ := strconv.Atoi(parts[1]) + builder.AddQuery(&QueryCondition{ + Field: esField, + Type: "range", + GTE: minVal, + LTE: maxVal, + }) + } + } +} + +// buildExactMatchConditions 构建精确匹配查询条件 +func (svc *BookService) buildExactMatchConditions(builder *ESQueryBuilder, request *request.BookSearchRequest) { + exactFields := map[string]string{ + "ISBN": request.ISBN, + "ID": request.ID, + "Publisher": request.Publisher, + } + + esFields := map[string]string{ + "ISBN": "isbn", + "ID": "id", + "Publisher": "publisher", + } + + for fieldName, value := range exactFields { + if value == "" { + continue + } + esField := esFields[fieldName] + builder.AddQuery(&QueryCondition{ + Field: esField, + Value: value, + Type: "term", + }) + } +} + +// buildFuzzyMatchConditions 构建模糊匹配查询条件 +func (svc *BookService) buildFuzzyMatchConditions(builder *ESQueryBuilder, request *request.BookSearchRequest) { + fuzzyFields := map[string]string{ + "BookName": request.BookName, + "Author": request.Author, + } + + esFields := map[string]string{ + "BookName": "book_name", + "Author": "author", + } + + for fieldName, value := range fuzzyFields { + if value == "" { + continue + } + esField := esFields[fieldName] + builder.AddQuery(&QueryCondition{ + Field: esField, + Value: value, + Type: "match", + Operator: "and", + }) + } +} + +// buildDefaultPrefixConditions 构建默认前缀匹配查询条件 +func (svc *BookService) buildDefaultPrefixConditions(builder *ESQueryBuilder, request *request.BookSearchRequest) { + prefixFields := map[string]string{ + "BindingLayout": request.BindingLayout, + "FixPrice": request.FixPrice, + } + + esFields := map[string]string{ + "BindingLayout": "binding_layout", + "FixPrice": "fix_price", + } + + for fieldName, value := range prefixFields { + if value == "" { + continue + } + esField := esFields[fieldName] + builder.AddQuery(&QueryCondition{ + Field: esField, + Value: value, + Type: "prefix", + }) + } +} + +// NewESQueryBuilder 创建 ES 查询构建器 +func NewESQueryBuilder() *ESQueryBuilder { + return &ESQueryBuilder{ + mustQueries: make([]map[string]interface{}, 0), + boolMust: make([]map[string]interface{}, 0), + boolMustNot: make([]map[string]interface{}, 0), + boolShould: make([]map[string]interface{}, 0), + } +} + +// AddQuery 添加单个查询条件 +func (b *ESQueryBuilder) AddQuery(cond *QueryCondition) { + var query map[string]interface{} + + switch cond.Type { + case "term": + query = map[string]interface{}{ + "term": map[string]interface{}{ + cond.Field: cond.Value, + }, + } + case "match": + matchQuery := map[string]interface{}{ + "query": cond.Value, + "operator": cond.Operator, + "fuzziness": "AUTO", + } + query = map[string]interface{}{ + "match": map[string]interface{}{ + cond.Field: matchQuery, + }, + } + case "match_phrase": + query = map[string]interface{}{ + "match_phrase": map[string]interface{}{ + cond.Field: cond.Value, + }, + } + case "prefix": + query = map[string]interface{}{ + "prefix": map[string]interface{}{ + cond.Field: cond.Value, + }, + } + case "range": + rangeCond := make(map[string]interface{}) + if cond.GTE != nil { + rangeCond["gte"] = cond.GTE + } + if cond.LTE != nil { + rangeCond["lte"] = cond.LTE + } + query = map[string]interface{}{ + "range": map[string]interface{}{ + cond.Field: rangeCond, + }, + } + case "wildcard": + query = map[string]interface{}{ + "wildcard": map[string]interface{}{ + cond.Field: cond.Pattern, + }, + } + case "exists": + query = map[string]interface{}{ + "exists": map[string]interface{}{ + "field": cond.Field, + }, + } + } + + if query != nil { + b.mustQueries = append(b.mustQueries, query) + log.Printf("[DEBUG] Added query: %v", query) + } +} + +// AddBoolQuery 添加布尔查询条件 +func (b *ESQueryBuilder) AddBoolQuery(boolType string, queries []map[string]interface{}, minShouldMatch ...int) { + switch boolType { + case "must": + b.boolMust = append(b.boolMust, queries...) + case "must_not": + b.boolMustNot = append(b.boolMustNot, queries...) + case "should": + b.boolShould = append(b.boolShould, queries...) + if len(minShouldMatch) > 0 { + b.minShouldMatch = minShouldMatch[0] + } + } + log.Printf("[DEBUG] Added bool %s queries: %v", boolType, queries) +} + +// Build 构建最终的 ES 查询 +func (b *ESQueryBuilder) Build(from, size int, sort []map[string]interface{}) map[string]interface{} { + allMust := make([]map[string]interface{}, 0) + allMust = append(allMust, b.mustQueries...) + allMust = append(allMust, b.boolMust...) + + query := map[string]interface{}{ + "from": from, + "size": size, + "sort": sort, + } + + boolQuery := make(map[string]interface{}) + if len(allMust) > 0 { + boolQuery["must"] = allMust + } + if len(b.boolMustNot) > 0 { + boolQuery["must_not"] = b.boolMustNot + } + if len(b.boolShould) > 0 { + boolQuery["should"] = b.boolShould + if b.minShouldMatch > 0 { + boolQuery["minimum_should_match"] = b.minShouldMatch + } + } + + query["query"] = map[string]interface{}{ + "bool": boolQuery, + } + + return query +} diff --git a/util/common.go b/util/common.go new file mode 100644 index 0000000..872eb23 --- /dev/null +++ b/util/common.go @@ -0,0 +1,47 @@ +package util + +import ( + "fmt" + "strings" + "time" +) + +// ParsePublicationTime 支持的格式:"2013-12", "2013", "2013-12-25", "2013 年 12 月" 等 +func ParsePublicationTime(timeStr string) int64 { + if timeStr == "" { + return 0 + } + + // 清理字符串中的空格和特殊字符 + timeStr = strings.TrimSpace(timeStr) + timeStr = strings.ReplaceAll(timeStr, "年", "-") + timeStr = strings.ReplaceAll(timeStr, "月", "") + timeStr = strings.TrimSpace(timeStr) + + // 定义多种可能的日期格式 + formats := []string{ + "2006-1", // "2013-12" + "2006-01", // "2013-12" (补零) + "2006", // "2013" + "2006-1-2", // "2013-12-25" + "2006-01-02", // "2013-12-25" (补零) + } + + var t time.Time + var err error + + for _, format := range formats { + t, err = time.ParseInLocation(format, timeStr, time.Local) + if err == nil { + break + } + } + + // 如果所有格式都失败,返回 0 + if err != nil { + fmt.Printf("出版时间解析失败:%s\n", timeStr) + return 0 + } + + return t.Unix() +}