package es import ( "bufio" "bytes" "centerBook/image" "centerBook/kongfz" "centerBook/model/request" "centerBook/monitor" "centerBook/tail" "centerBook/util/redisClient" "context" "encoding/json" "fmt" "io" "log" "net/http" "strconv" "strings" "time" "github.com/gin-gonic/gin" jsoniter "github.com/json-iterator/go" "github.com/elastic/go-elasticsearch/v8/esapi" ) const ESIndex = "books-from-mysql-v3" // ESBookResponse 用于返回给Java客户端的格式,ID为简单的int64 type ESBookResponse 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 处理可能是字符串或数组的字段 type FlexibleString struct { Value string } // NewFlexibleString 创建一个FlexibleString实例 func NewFlexibleString(value string) FlexibleString { return FlexibleString{Value: value} } func (f *FlexibleString) UnmarshalJSON(data []byte) error { // 尝试解析为单个字符串 var single string if err := json.Unmarshal(data, &single); err == nil { f.Value = single return nil } // 尝试解析为数组 var arr []string if err := json.Unmarshal(data, &arr); err == nil { if len(arr) > 0 { f.Value = arr[0] // 取第一个值 } return nil } return fmt.Errorf("cannot parse string: %s", string(data)) } // MarshalJSON 实现JSON序列化,返回字符串而不是对象 func (f *FlexibleString) MarshalJSON() ([]byte, error) { log.Printf("[FlexibleString MarshalJSON] 正在序列化: %s", f.Value) return json.Marshal(f.Value) } // FlexibleID 处理ID字段可能是数组或单个值的情况 type FlexibleID struct { Value int64 } // NewFlexibleID 创建一个FlexibleID实例 func NewFlexibleID(value int64) FlexibleID { return FlexibleID{Value: value} } func (f *FlexibleID) UnmarshalJSON(data []byte) error { // 尝试解析为单个数字 var single int64 if err := json.Unmarshal(data, &single); err == nil { f.Value = single return nil } // 尝试解析为数组 var arr []int64 if err := json.Unmarshal(data, &arr); err == nil { if len(arr) > 0 { f.Value = arr[0] // 取第一个值 } return nil } // 尝试解析为字符串数字 var str string if err := json.Unmarshal(data, &str); err == nil { if val, err := strconv.ParseInt(str, 10, 64); err == nil { f.Value = val return nil } } return fmt.Errorf("cannot parse ID: %s", string(data)) } // ConvertToResponse 将ESBook转换为ESBookResponse,处理ID字段转换 func (book *ESBook) ConvertToResponse() ESBookResponse { 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") } return ESBookResponse{ 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 type BookPicObj struct { LocalPath string `json:"localPath"` PddPath string `json:"pddPath"` } // 用于 book_pic_s type BookPicSObj struct { LocalPath string `json:"localPath"` PddResponse string `json:"pddResponse"` } // 用于 book_def_pic type BookDefPicObj struct { LocalPath string `json:"localPath"` PddPath string `json:"pddPath"` } type ESBook struct { ID int64 `json:"id,omitempty"` BookName FlexibleString `json:"book_name,omitempty"` BookPic BookPicObj `json:"book_pic,omitempty"` BookPicS BookPicSObj `json:"book_pic_s,omitempty"` BookPicB string `json:"book_pic_b,omitempty"` BookPicW map[string]interface{} `json:"book_pic_w,omitempty"` // 新增自制默认官图字段 BookDefPic BookDefPicObj `json:"book_def_pic,omitempty"` ISBN string `json:"isbn,omitempty"` Author string `json:"author,omitempty"` Category string `json:"category,omitempty"` Publisher string `json:"publisher,omitempty"` PublicationTime string `json:"publication_time,omitempty"` BindingLayout string `json:"binding_layout,omitempty"` FixPrice Float64OrString `json:"fix_price,omitempty"` Content string `json:"content,omitempty"` IsSuit int `json:"is_suit,omitempty"` // ⭐ 新增的字段 DaySale7 int `json:"day_sale_7,omitempty"` DaySale15 int `json:"day_sale_15,omitempty"` DaySale30 int `json:"day_sale_30,omitempty"` DaySale60 int `json:"day_sale_60,omitempty"` DaySale90 int `json:"day_sale_90,omitempty"` DaySale180 int `json:"day_sale_180,omitempty"` DaySale365 int `json:"day_sale_365,omitempty"` ThisYearSale int `json:"this_year_sale,omitempty"` LastYearSale int `json:"last_year_sale,omitempty"` TotalSale int `json:"total_sale,omitempty"` BuyCounts int64 `json:"buy_counts,omitempty"` SellCounts int64 `json:"sell_counts,omitempty"` BookPicObj map[string]interface{} `json:"book_pic_obj,omitempty"` BookPicObjS map[string]interface{} `json:"book_pic_obj_s,omitempty"` UpdateTime NumberOrString `json:"update_time,omitempty"` 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"` // 类目 Other map[string]interface{} `json:"other"` // 扩展字段,用于兼容未来新增字段 } // AddBookRequest 用于 Service 方法的入参 type AddBookRequest struct { BookName string `json:"book_name"` Author string `json:"author"` Publisher string `json:"publisher"` ISBN string `json:"isbn"` BookPic string `json:"book_pic"` BookPicS string `json:"book_pic_s"` BookPicNew string `json:"Book_pic_new"` BindingLayout string `json:"binding_layout"` FixPrice float64 `json:"fix_price"` PublicationTime string `json:"publication_time"` } // AddBookFullRequest 新的完整插入请求结构体,book_name强制为string类型 type AddBookFullRequest struct { BookName string `json:"book_name"` // 强制要求string类型 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"` // 水印图 ISBN string `json:"isbn"` // 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"` // 是否套装: 0否, 1是 DaySale7 int `json:"day_sale_7"` // 7天销量 DaySale15 int `json:"day_sale_15"` // 15天销量 DaySale30 int `json:"day_sale_30"` // 30天销量 DaySale60 int `json:"day_sale_60"` // 60天销量 DaySale90 int `json:"day_sale_90"` // 90天销量 DaySale180 int `json:"day_sale_180"` // 180天销量 DaySale365 int `json:"day_sale_365"` // 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"` // 小图对象 IsIllegal int `json:"is_illegal"` // 是否非法 示例 000000 IsReturn int `json:"is_return"` // 是否为驳回 示例 0 否 1 是 IsFilter string `json:"is_filter"` // 过滤字段 } type ESSearchService struct { ES *ESClient } type NumberOrString string func (n *NumberOrString) UnmarshalJSON(data []byte) error { s := strings.TrimSpace(string(data)) // 如果是数字(不以引号开头) if len(s) > 0 && s[0] != '"' { *n = NumberOrString(s) return nil } // 普通字符串 var str string if err := json.Unmarshal(data, &str); err != nil { return err } *n = NumberOrString(str) return nil } type Float64OrString float64 func (f *Float64OrString) UnmarshalJSON(b []byte) error { // 去掉空值 if string(b) == "null" || len(b) == 0 { *f = 0 return nil } // 尝试解析为 float var num float64 if err := json.Unmarshal(b, &num); err == nil { *f = Float64OrString(num) return nil } // 尝试解析为 string var str string if err := json.Unmarshal(b, &str); err == nil { if str == "" { *f = 0 return nil } parsed, err := strconv.ParseFloat(str, 64) if err != nil { return err } *f = Float64OrString(parsed) return nil } return fmt.Errorf("无法解析 fix_price: %s", string(b)) } func NewESSearchService(es *ESClient) *ESSearchService { return &ESSearchService{ES: es} } // validateBookPicObj 验证 BookPic 对象格式 func validateBookPicObj(bookPic map[string]interface{}, fieldName string) error { // 允许book_pic为nil,直接返回成功 if bookPic == nil { return nil } // 检查字段是否存在,不存在则使用默认空字符串 localPath, hasLocalPath := bookPic["localPath"] if !hasLocalPath { localPath = "" bookPic["localPath"] = "" } pddPath, hasPddPath := bookPic["pddPath"] if !hasPddPath { pddPath = "" bookPic["pddPath"] = "" } // 检查字段类型,允许空字符串,但必须是字符串类型 if _, ok := localPath.(string); !ok { return fmt.Errorf("%s localPath 必须为字符串类型", fieldName) } if _, ok := pddPath.(string); !ok { return fmt.Errorf("%s pddPath 必须为字符串类型", fieldName) } return nil } // validateBookPicSObj 验证 BookPicS 对象格式 func validateBookPicSObj(bookPicS map[string]interface{}, fieldName string) error { // 允许book_pic_s为nil,直接返回成功 if bookPicS == nil { return nil } // 检查字段是否存在,不存在则使用默认空字符串 localPath, hasLocalPath := bookPicS["localPath"] if !hasLocalPath { localPath = "" bookPicS["localPath"] = "" } pddResponse, hasPddResponse := bookPicS["pddResponse"] if !hasPddResponse { pddResponse = "" bookPicS["pddResponse"] = "" } // 检查字段类型,允许空字符串,但必须是字符串类型 if _, ok := localPath.(string); !ok { return fmt.Errorf("%s localPath 必须为字符串类型", fieldName) } if _, ok := pddResponse.(string); !ok { return fmt.Errorf("%s pddResponse 必须为字符串类型", fieldName) } return nil } // validateBookPicWObj 验证 BookPicW 对象格式 func validateBookPicWObj(bookPicW map[string]interface{}, fieldName string) error { if bookPicW == nil { return fmt.Errorf("%s 不能为 nil", fieldName) } // BookPicW 是任意对象,只需要验证不是空map if len(bookPicW) == 0 { return fmt.Errorf("%s 不能为空对象", fieldName) } return nil } // validateBookPicObjS 验证 BookPicObjS 对象格式(类似于 BookPicS) func validateBookPicObjS(bookPicObjS map[string]interface{}, fieldName string) error { return validateBookPicSObj(bookPicObjS, fieldName) } type esHitsWrapper struct { Hits struct { Total struct { Value int `json:"value"` } `json:"total"` Hits []struct { Index string `json:"_index"` ID string `json:"_id"` Source ESBook `json:"_source"` } `json:"hits"` } `json:"hits"` } // SearchBooks 搜索图书 func (svc *ESSearchService) SearchBooks(keyword string, endpoint ...string) ([]ESBook, error) { keyword = strings.TrimSpace(keyword) if keyword == "" { return []ESBook{}, nil } query := map[string]interface{}{ "_source": []string{ "id", "book_name", "book_pic", "book_pic_s", "isbn", "author", "category", "publisher", "publication_time", "binding_layout", "fix_price", "content", "update_time", }, "query": map[string]interface{}{ "multi_match": map[string]interface{}{ "query": keyword, "type": "best_fields", "fields": []string{"book_name^5", "isbn^10", "author^3"}, "fuzziness": "AUTO", }, }, } body, err := json.Marshal(query) if err != nil { return nil, fmt.Errorf("序列化请求失败: %v", err) } req := esapi.SearchRequest{ Index: []string{ESIndex}, Body: bytes.NewReader(body), TrackTotalHits: true, } // 如果有传入 endpoint,使用监控 var res *esapi.Response var duration time.Duration if len(endpoint) > 0 && endpoint[0] != "" { monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint[0]) res, duration, err = monitoredES.Search(context.Background(), &req) log.Printf("[SearchBooks] ES 查询耗时:%dms", duration.Milliseconds()) } else { // 原有逻辑,不带监控 res, err = req.Do(context.Background(), svc.ES.Client.Transport) } if err != nil { return nil, fmt.Errorf("执行 ES 查询失败: %v", err) } defer res.Body.Close() if res.IsError() { return nil, fmt.Errorf("ES 返回错误: %s", res.String()) } var parsed esHitsWrapper if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { return nil, fmt.Errorf("解析 ES 响应失败: %v", err) } list := make([]ESBook, 0, len(parsed.Hits.Hits)) for _, hit := range parsed.Hits.Hits { list = append(list, hit.Source) } return list, nil } // SearchBookByISBN 通过 ISBN 查询图书 func (svc *ESSearchService) SearchBookByISBN(isbn string) (*ESBook, error) { isbn = strings.TrimSpace(isbn) log.Printf("[SearchBookByISBN] 开始查询 | ISBN=%s", isbn) if isbn == "" { log.Printf("[SearchBookByISBN] ISBN 为空,取消查询") return nil, fmt.Errorf("ISBN 不能为空") } query := map[string]interface{}{ "query": map[string]interface{}{ "term": map[string]interface{}{ "isbn": isbn, // 精准匹配 }, }, "_source": true, } body, err := json.Marshal(query) if err != nil { log.Printf("[SearchBookByISBN] 构建查询 JSON 失败: %v", err) return nil, fmt.Errorf("构建查询 JSON 失败: %v", err) } // 执行查询 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), ) if err != nil { log.Printf("[SearchBookByISBN] ES 查询失败: %v", err) return nil, fmt.Errorf("ES 查询失败: %v", err) } defer res.Body.Close() //log.Printf("[SearchBookByISBN] ES 响应: %s", res.String()) 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 } func (svc *ESSearchService) SearchBookByBookName(bookName string) ([]ESBook, error) { bookName = strings.TrimSpace(bookName) if bookName == "" { return nil, fmt.Errorf("bookName 不能为空") } query := map[string]interface{}{ "_source": true, "query": map[string]interface{}{ "match": map[string]interface{}{ "book_name": map[string]interface{}{ "query": bookName, "operator": "and", "fuzziness": "AUTO", }, }, }, } body, _ := json.Marshal(query) 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), ) if err != nil { return nil, fmt.Errorf("ES 查询失败: %v", err) } defer res.Body.Close() if res.IsError() { return nil, fmt.Errorf("ES 返回错误: %s", res.String()) } var parsed esHitsWrapper if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { return nil, fmt.Errorf("解析 ES 返回失败: %v", err) } list := make([]ESBook, 0, len(parsed.Hits.Hits)) for _, hit := range parsed.Hits.Hits { list = append(list, hit.Source) } return list, nil } // SearchBooksAllFields 单字段匹配 func (svc *ESSearchService) SearchBooksAllFields(keyword string) ([]ESBook, error) { keyword = strings.TrimSpace(keyword) if keyword == "" { return []ESBook{}, nil } query := map[string]interface{}{ "_source": true, "query": map[string]interface{}{ "multi_match": map[string]interface{}{ "query": keyword, "type": "best_fields", // 使用最佳字段评分模式 "fields": []string{ "id^1", "book_name^5", "book_pic^1", "book_pic_s^1", "isbn^10", "author^4", "category^2", "publisher^3", "publication_time^1", "binding_layout^1", "fix_price^1", "content^1", "update_time^1", }, "fuzziness": "AUTO", // 自动纠错/模糊匹配 }, }, } body, _ := json.Marshal(query) 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), ) if err != nil { return nil, fmt.Errorf("执行 ES 查询失败: %v", err) } defer res.Body.Close() if res.IsError() { return nil, fmt.Errorf("ES 返回错误: %s", res.String()) } var parsed esHitsWrapper if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { return nil, fmt.Errorf("解析 ES 响应失败: %v", err) } list := make([]ESBook, 0, len(parsed.Hits.Hits)) for _, hit := range parsed.Hits.Hits { list = append(list, hit.Source) } return list, nil } // SearchBooksByConditions 多字段AND搜索 func (svc *ESSearchService) SearchBooksByConditions(conds map[string]string, page, pageSize int) ([]ESBook, int, error) { mustQueries := make([]map[string]interface{}, 0) for field, value := range conds { value = strings.TrimSpace(value) if value == "" { continue } // 特殊处理 day_sale_30 做范围查询 if field == "day_sale_30" { // 这里假设前端传入 "1",表示查询 >1 if v, err := strconv.Atoi(value); err == nil { mustQueries = append(mustQueries, map[string]interface{}{ "range": map[string]interface{}{ "day_sale_30": map[string]interface{}{ "gt": v, }, }, }) } continue } // 其他字段用 match 查询 mustQueries = append(mustQueries, map[string]interface{}{ "match": map[string]interface{}{ field: map[string]interface{}{ "query": value, "operator": "and", "fuzziness": "AUTO", }, }, }) } if len(mustQueries) == 0 { return []ESBook{}, 0, nil } from := (page - 1) * pageSize query := map[string]interface{}{ "_source": true, "from": from, "size": pageSize, "query": map[string]interface{}{ "bool": map[string]interface{}{ "must": mustQueries, }, }, } body, _ := json.Marshal(query) 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), ) if err != nil { return nil, 0, fmt.Errorf("ES 查询失败: %v", err) } defer res.Body.Close() if res.IsError() { return nil, 0, fmt.Errorf("ES 错误: %s", res.String()) } var parsed esHitsWrapper if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { return nil, 0, fmt.Errorf("解析失败: %v", err) } list := make([]ESBook, 0, len(parsed.Hits.Hits)) for _, hit := range parsed.Hits.Hits { list = append(list, hit.Source) } return list, parsed.Hits.Total.Value, nil } // CountByIDRange 统计 ID 范围内的数量 func (svc *ESSearchService) CountByIDRange(minID, maxID int) (int, error) { query := map[string]interface{}{ "query": map[string]interface{}{ "range": map[string]interface{}{ "id": map[string]interface{}{ "gte": minID, "lte": maxID, }, }, }, } body, _ := json.Marshal(query) res, err := svc.ES.Client.Count( svc.ES.Client.Count.WithIndex(ESIndex), svc.ES.Client.Count.WithBody(bytes.NewReader(body)), ) if err != nil { return 0, err } defer res.Body.Close() if res.IsError() { return 0, fmt.Errorf("ES 错误: %s", res.String()) } var resp struct { Count int `json:"count"` } if err := json.NewDecoder(res.Body).Decode(&resp); err != nil { return 0, err } return resp.Count, nil } // UpdateBookPicByISBN 根据 ISBN 更新 ES 文档的图片字段 // 功能:更新 `book_pic` 与 `book_pic_s` 字段(任意一个或两个) // 入参: // - isbn: 目标文档的 ISBN(精确匹配) // - bookPic: 新的大图地址(可为空,空则不更新该字段) // - bookPicS: 新的小图地址(可为空,空则不更新该字段) // 返回:成功更新的文档数量 func (svc *ESSearchService) UpdateBookPicByISBN(isbn, bookPic, bookPicS string) (int, error) { isbn = strings.TrimSpace(isbn) if isbn == "" { return 0, fmt.Errorf("ISBN 不能为空") } // 构建脚本:仅对提供的字段进行更新 scriptParts := make([]string, 0, 2) if strings.TrimSpace(bookPic) != "" { scriptParts = append(scriptParts, "ctx._source.book_pic = params.book_pic;") } if strings.TrimSpace(bookPicS) != "" { scriptParts = append(scriptParts, "ctx._source.book_pic_s = params.book_pic_s;") } if len(scriptParts) == 0 { return 0, fmt.Errorf("至少提供 book_pic 或 book_pic_s 其中之一") } source := strings.Join(scriptParts, " ") body := map[string]interface{}{ "script": map[string]interface{}{ "source": source, "lang": "painless", "params": map[string]interface{}{ "book_pic": bookPic, "book_pic_s": bookPicS, }, }, "query": map[string]interface{}{ "term": map[string]interface{}{ "isbn": isbn, }, }, } payload, _ := json.Marshal(body) res, err := svc.ES.Client.UpdateByQuery( []string{ESIndex}, svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)), svc.ES.Client.UpdateByQuery.WithRefresh(true), svc.ES.Client.UpdateByQuery.WithConflicts("proceed"), ) if err != nil { return 0, fmt.Errorf("ES UpdateByQuery 执行失败: %v", err) } defer res.Body.Close() if res.IsError() { return 0, fmt.Errorf("ES 返回错误: %s", res.String()) } // 解析更新数量 var parsed struct { Total int `json:"total"` Updated int `json:"updated"` VersionConflicts int `json:"version_conflicts"` } if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { return 0, fmt.Errorf("解析 ES UpdateByQuery 响应失败: %v", err) } return parsed.Updated, nil } // UpdateSellCountsByISBN 根据 ISBN 更新 ES 文档的 sell_counts 字段 // 功能:将指定 ISBN 的文档字段 `sell_counts` 更新为给定值 // 入参: // - isbn: 精确匹配目标文档的 ISBN // - sellCounts: 在售数量(非负整数) // 返回:成功更新的文档数量 func (svc *ESSearchService) UpdateSellCountsByISBN(isbn string, sellCounts int) (int, error) { isbn = strings.TrimSpace(isbn) if isbn == "" { return 0, fmt.Errorf("ISBN 不能为空") } if sellCounts < 0 { return 0, fmt.Errorf("sell_counts 不可为负数") } body := map[string]interface{}{ "script": map[string]interface{}{ "source": "ctx._source.sell_counts = params.sell_counts;", "lang": "painless", "params": map[string]interface{}{ "sell_counts": sellCounts, }, }, "query": map[string]interface{}{ "term": map[string]interface{}{ "isbn": isbn, }, }, } payload, _ := json.Marshal(body) res, err := svc.ES.Client.UpdateByQuery( []string{ESIndex}, svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)), svc.ES.Client.UpdateByQuery.WithRefresh(true), svc.ES.Client.UpdateByQuery.WithConflicts("proceed"), ) if err != nil { return 0, fmt.Errorf("ES UpdateByQuery 执行失败: %v", err) } defer res.Body.Close() if res.IsError() { return 0, fmt.Errorf("ES 返回错误: %s", res.String()) } var parsed struct { Total int `json:"total"` Updated int `json:"updated"` VersionConflicts int `json:"version_conflicts"` } if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { return 0, fmt.Errorf("解析 ES UpdateByQuery 响应失败: %v", err) } return parsed.Updated, nil } // UpdateSellCountsByISBNHandler 根据请求的 ISBN 调用外部接口获取在售数量并更新 ES // 功能: // 1) 读取 `isbn` 参数; // 2) 调用 tail.GetOnSaleCount 获取在售数量; // 3) 调用 UpdateSellCountsByISBN 更新 ES; // 4) 返回更新结果与在售数量。 func (svc *ESSearchService) UpdateSellCountsByISBNHandler(c *gin.Context) { // 异步执行:立即返回成功,后台更新 ES isbn := strings.TrimSpace(c.Query("isbn")) if isbn == "" { c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}}) return } // 获取在售数量(容错 data:{}) onSaleCount, err := tail.GetOnSaleCount(isbn) log.Printf("[Async] 获取在售数量 | ISBN=%s | count=%d", isbn, onSaleCount) if err != nil { c.JSON(500, gin.H{"error": "获取在售数量失败", "details": err.Error()}) return } if onSaleCount == 0 { c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}}) return } // 后台异步更新 go func(isbn string, count int) { updated, err := svc.UpdateSellCountsByISBN(isbn, count) if err != nil { log.Printf("[Async] 更新 ES 失败 | ISBN=%s | err=%v", isbn, err) return } log.Printf("[Async] 更新 ES 成功 | ISBN=%s | sell_counts=%d | updated=%d", isbn, count, updated) }(isbn, onSaleCount) // 立即返回成功,携带异步提示 c.JSON(200, gin.H{ "code": 200, "message": "success", "data": gin.H{"isbn": isbn, "on_sale_count": onSaleCount, "async": true}, }) } // UpdateSellCountsDirectHandler 直接按入参异步更新 ES 的在售数量 // 功能: // - 从请求中读取 `isbn` 与 `onSaleCount`; // - 校验参数(ISBN 非空、onSaleCount >= 0 且为整数); // - 后台异步调用 ES 更新 `sell_counts` 字段; // - 立即返回标准成功结构,包含 async 提示;当 onSaleCount=0 时直接返回 success 不更新。 func (svc *ESSearchService) UpdateSellCountsDirectHandler(c *gin.Context) { isbn := strings.TrimSpace(c.Query("isbn")) countStr := strings.TrimSpace(c.Query("onSaleCount")) if isbn == "" { c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) return } if countStr == "" { c.JSON(400, gin.H{"error": "缺少 onSaleCount 参数"}) return } count, err := strconv.Atoi(countStr) if err != nil { c.JSON(400, gin.H{"error": "onSaleCount 必须为整数", "details": err.Error()}) return } if count < 0 { c.JSON(400, gin.H{"error": "onSaleCount 不可为负数"}) return } // 如果为 0,直接返回 success,不做更新 if count == 0 { c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}}) return } // 后台异步更新 ES 的 sell_counts 字段 go func(isbn string, c int) { updated, err := svc.UpdateSellCountsByISBN(isbn, c) if err != nil { log.Printf("[Async] 直接更新 ES 失败 | ISBN=%s | err=%v", isbn, err) return } log.Printf("[Async] 直接更新 ES 成功 | ISBN=%s | sell_counts=%d | updated=%d", isbn, c, updated) }(isbn, count) // 立即返回成功,携带异步提示 c.JSON(200, gin.H{ "code": 200, "message": "success", "data": gin.H{"isbn": isbn, "sell_counts": count, "async": true}, }) } // SearchBookBaseInfoES 根据条件查询 ES 图书信息 func (svc *ESSearchService) SearchBookBaseInfoES(c *gin.Context) ([]ESBook, int, error) { q := c.Request.URL.Query() must := make([]map[string]interface{}, 0) // ES 字段映射 snakeMap := map[string]string{ "bookName": "book_name", "bookPic": "book_pic", "publication_times": "publication_time", "isSuit": "is_suit", } // ===== saleSelect 对应字段映射 ===== saleSelect := c.DefaultQuery("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", }[saleSelect] fmt.Printf("[DEBUG] saleSelect=%s saleField=%s\n", saleSelect, saleField) // saleField >= 1 默认条件 if saleField != "" { cond := map[string]interface{}{ "range": map[string]interface{}{ saleField: map[string]interface{}{"gte": 1}, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } // per_page → pageSize if perPage := c.Query("per_page"); perPage != "" { q.Set("pageSize", perPage) } // ========== 遍历参数 ========== for key, vals := range q { val := strings.TrimSpace(vals[0]) if val == "" { continue } if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" || key == "shopType" { continue } // 忽略无用字段 if key == "is_pricing" || key == "vio_book" || key == "book_set" || key == "onenum_mbooks" || key == "ill_publisher" || key == "ill_author" { continue } originalKey := key fmt.Printf("[DEBUG] Processing key=%s val=%s\n", key, val) // 字段映射 if nk, ok := snakeMap[key]; ok { key = nk } // ===== is_suit ===== if key == "is_suit" { fmt.Printf("[DEBUG] is_suit val=%q\n", val) if num, err := strconv.Atoi(val); err == nil { cond := map[string]interface{}{ "term": map[string]interface{}{"is_suit": num}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } else { fmt.Printf("[ERROR] is_suit Atoi error: %v\n", err) } continue } // ===== is_return ===== if key == "is_return" { fmt.Printf("[DEBUG] is_return val=%q\n", val) if num, err := strconv.Atoi(val); err == nil { cond := map[string]interface{}{ "term": map[string]interface{}{"is_return": num}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } else { fmt.Printf("[ERROR] is_return Atoi error: %v\n", err) } continue } // ===== is_filter ===== // ===== is_filter(按 shopType 位匹配)===== // ===== is_filter(按 shopType 位匹配)===== if key == "is_filter" { fmt.Printf("[DEBUG] is_filter val=%q\n", val) // 只处理 1(=1) 和 2(=0) if val != "1" && val != "2" { continue } shopType := c.DefaultQuery("shopType", "") var pattern string // val=1 -> 位=1 // val=2 -> 位=0 targetBit := "1" if val == "2" { targetBit = "0" } switch shopType { case "0": pattern = targetBit + "*" case "1": pattern = "?" + targetBit + "*" case "2": pattern = "??" + targetBit + "*" case "3": pattern = "???" + targetBit + "*" default: continue } cond := map[string]interface{}{ "wildcard": map[string]interface{}{ "is_filter": pattern, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== categoryType ===== if originalKey == "categoryType" { var cond map[string]interface{} if val == "1" { cond = map[string]interface{}{ "prefix": map[string]interface{}{"isbn": "9787"}, } } else { cond = map[string]interface{}{ "bool": map[string]interface{}{ "must_not": []map[string]interface{}{ {"prefix": map[string]interface{}{"isbn": "9787"}}, }, }, } } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== category ===== if key == "category" { var cond map[string]interface{} if val == "排除大学教材" { cond = map[string]interface{}{ "bool": map[string]interface{}{ "must_not": []map[string]interface{}{ {"match_phrase": map[string]interface{}{ "category": "图书/教材教辅考试/大学教材", }}, }, }, } } else { cond = map[string]interface{}{ "match_phrase": map[string]interface{}{"category": val}, } } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===================================================== // ========== ★★★ book_pic + picType 联动逻辑 ★★★ ========== // ===================================================== if key == "book_pic" { picType := c.DefaultQuery("picType", "1") // 默认官图 var targetField string if picType == "1" { targetField = "book_pic.pddPath" } else { targetField = "book_pic_s.pddResponse" } var cond map[string]interface{} if val == "1" { // 有图:字段必须存在且非空 cond = map[string]interface{}{ "bool": map[string]interface{}{ "must": []map[string]interface{}{ {"exists": map[string]interface{}{"field": targetField}}, {"wildcard": map[string]interface{}{ targetField: "*", }}, }, }, } } else { // 无图:使用keyword字段查询字段不存在或为空字符串 var keywordField string if picType == "1" { keywordField = "book_pic.pddPath.keyword" } else { keywordField = "book_pic_s.pddResponse.keyword" } cond = map[string]interface{}{ "bool": map[string]interface{}{ "should": []map[string]interface{}{ // 情况1:字段完全不存在 { "bool": map[string]interface{}{ "must_not": []map[string]interface{}{ {"exists": map[string]interface{}{"field": keywordField}}, }, }, }, // 情况2:keyword字段存在但为空字符串 { "term": map[string]interface{}{ keywordField: "", }, }, }, "minimum_should_match": 1, }, } } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== buy_counts → saleField ===== if key == "buy_counts" { fmt.Printf("[DEBUG] buy_counts uses saleField=%s\n", saleField) if saleField == "" { continue } 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{}{ saleField: map[string]interface{}{ "gte": minVal, "lte": maxVal, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } if num, err := strconv.Atoi(val); err == nil { cond := map[string]interface{}{ "range": map[string]interface{}{ saleField: map[string]interface{}{ "gte": num, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } } // ===== totalSale_range ===== if key == "totalSale_range" { 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{}{ "total_sale": map[string]interface{}{ "gte": minVal, "lte": maxVal, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } } // ===== 数值范围 ===== if key == "sell_counts" || strings.HasPrefix(key, "day_sale_") || key == "this_year_sale" || key == "last_year_sale" || key == "publication_time" { 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{}{ key: map[string]interface{}{ "gte": minVal, "lte": maxVal, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } } // ===== 精确匹配 ===== if key == "isbn" || key == "id" || key == "publisher" { cond := map[string]interface{}{ "term": map[string]interface{}{key: val}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== 模糊匹配 ===== if key == "book_name" || key == "author" { cond := map[string]interface{}{ "match": map[string]interface{}{ key: map[string]interface{}{ "query": val, "operator": "and", "fuzziness": "AUTO", }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== 默认前缀匹配 ===== cond := map[string]interface{}{ "prefix": map[string]interface{}{key: val}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } // ========== 分页 ========== page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10"))) from := (page - 1) * pageSize // ========== pageSize大于500则根据id查询 ========= fmt.Printf("[DEBUG] page=%d pageSize=%d from=%d\n", page, pageSize, from) var sort []map[string]interface{} if 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 := map[string]interface{}{ "from": from, "size": pageSize, "query": map[string]interface{}{ "bool": map[string]interface{}{ "must": must, }, }, "sort": sort, } body, _ := json.MarshalIndent(query, "", " ") 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), ) if err != nil { fmt.Printf("[ERROR] ES.Client.Search error: %v\n", err) return nil, 0, err } 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 // 使用CopyBuffer可以重用缓冲区 writer := bufio.NewWriterSize(&buf, 8192) _, err = io.Copy(writer, res.Body) if err != nil { return nil, 0, fmt.Errorf("复制响应数据失败: %v", err) } rawData := buf.Bytes() //rawData, err := io.ReadAll(res.Body) 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))])) } //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)) for _, hit := range parsed.Hits.Hits { list = append(list, hit.Source) } return list, parsed.Hits.Total.Value, nil } func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, int, error) { q := c.Request.URL.Query() must := make([]map[string]interface{}, 0) // ES 字段映射 snakeMap := map[string]string{ "bookName": "book_name", "bookPic": "book_pic", "publication_times": "publication_time", "isSuit": "is_suit", } // ===== saleSelect 对应字段映射 ===== saleSelect := c.DefaultQuery("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", }[saleSelect] fmt.Printf("[DEBUG] saleSelect=%s saleField=%s\n", saleSelect, saleField) // saleField >= 1 默认条件 if saleField != "" { cond := map[string]interface{}{ "range": map[string]interface{}{ saleField: map[string]interface{}{"gte": 1}, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } // per_page → pageSize if perPage := c.Query("per_page"); perPage != "" { q.Set("pageSize", perPage) } // ========== 遍历参数 ========== for key, vals := range q { val := strings.TrimSpace(vals[0]) if val == "" { continue } // 不作为查询条件的字段 if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" || key == "shopType" { continue } // 忽略无用字段 if key == "is_pricing" || key == "vio_book" || key == "book_set" || key == "onenum_mbooks" || key == "ill_publisher" || key == "ill_author" { continue } originalKey := key fmt.Printf("[DEBUG] Processing key=%s val=%s\n", key, val) // 字段映射 if nk, ok := snakeMap[key]; ok { key = nk } // ===== is_suit ===== if key == "is_suit" { fmt.Printf("[DEBUG] is_suit val=%q\n", val) if num, err := strconv.Atoi(val); err == nil { cond := map[string]interface{}{ "term": map[string]interface{}{"is_suit": num}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } else { fmt.Printf("[ERROR] is_suit Atoi error: %v\n", err) } continue } // ===== is_return ===== if key == "is_return" { fmt.Printf("[DEBUG] is_return val=%q\n", val) if num, err := strconv.Atoi(val); err == nil { cond := map[string]interface{}{ "term": map[string]interface{}{"is_return": num}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } else { fmt.Printf("[ERROR] is_return Atoi error: %v\n", err) } continue } // ===== is_filter ===== // ===== is_filter(按 shopType 位匹配)===== if key == "is_filter" { fmt.Printf("[DEBUG] is_filter val=%q\n", val) // 只处理 1(=1) 和 2(=0) if val != "1" && val != "2" { continue } shopType := c.DefaultQuery("shopType", "") var pattern string // val=1 -> 位=1 // val=2 -> 位=0 targetBit := "1" if val == "2" { targetBit = "0" } switch shopType { case "0": pattern = targetBit + "*" case "1": pattern = "?" + targetBit + "*" case "2": pattern = "??" + targetBit + "*" case "3": pattern = "???" + targetBit + "*" default: continue } cond := map[string]interface{}{ "wildcard": map[string]interface{}{ "is_filter": pattern, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== categoryType ===== if originalKey == "categoryType" { var cond map[string]interface{} if val == "1" { cond = map[string]interface{}{ "prefix": map[string]interface{}{"isbn": "9787"}, } } else { cond = map[string]interface{}{ "bool": map[string]interface{}{ "must_not": []map[string]interface{}{ {"prefix": map[string]interface{}{"isbn": "9787"}}, }, }, } } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== category ===== if key == "category" { var cond map[string]interface{} if val == "排除大学教材" { cond = map[string]interface{}{ "bool": map[string]interface{}{ "must_not": []map[string]interface{}{ {"match_phrase": map[string]interface{}{ "category": "图书/教材教辅考试/大学教材", }}, }, }, } } else { cond = map[string]interface{}{ "match_phrase": map[string]interface{}{"category": val}, } } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===================================================== // ========== ★★★ book_pic + picType 联动逻辑 ★★★ ========== // ===================================================== if key == "book_pic" { picType := c.DefaultQuery("picType", "1") // 默认官图 var targetField string if picType == "1" { targetField = "book_pic.pddPath" } else { targetField = "book_pic_s.pddResponse" } var cond map[string]interface{} if val == "1" { // 有图:字段必须存在且非空 cond = map[string]interface{}{ "bool": map[string]interface{}{ "must": []map[string]interface{}{ {"exists": map[string]interface{}{"field": targetField}}, {"wildcard": map[string]interface{}{ targetField: "*", }}, }, }, } } else { // 无图:使用keyword字段查询字段不存在或为空字符串 var keywordField string if picType == "1" { keywordField = "book_pic.pddPath.keyword" } else { keywordField = "book_pic_s.pddResponse.keyword" } cond = map[string]interface{}{ "bool": map[string]interface{}{ "should": []map[string]interface{}{ // 情况1:字段完全不存在 { "bool": map[string]interface{}{ "must_not": []map[string]interface{}{ {"exists": map[string]interface{}{"field": keywordField}}, }, }, }, // 情况2:keyword字段存在但为空字符串 { "term": map[string]interface{}{ keywordField: "", }, }, }, "minimum_should_match": 1, }, } } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== buy_counts → saleField ===== if key == "buy_counts" { fmt.Printf("[DEBUG] buy_counts uses saleField=%s\n", saleField) if saleField == "" { continue } 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{}{ saleField: map[string]interface{}{ "gte": minVal, "lte": maxVal, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } if num, err := strconv.Atoi(val); err == nil { cond := map[string]interface{}{ "range": map[string]interface{}{ saleField: map[string]interface{}{ "gte": num, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } } // ===== totalSale_range ===== if key == "totalSale_range" { 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{}{ "total_sale": map[string]interface{}{ "gte": minVal, "lte": maxVal, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } } // ===== 数值范围 ===== if key == "sell_counts" || strings.HasPrefix(key, "day_sale_") || key == "this_year_sale" || key == "last_year_sale" || key == "publication_time" { 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{}{ key: map[string]interface{}{ "gte": minVal, "lte": maxVal, }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } } // ===== 精确匹配 ===== if key == "isbn" || key == "id" || key == "publisher" { cond := map[string]interface{}{ "term": map[string]interface{}{key: val}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== 模糊匹配 ===== if key == "book_name" || key == "author" { cond := map[string]interface{}{ "match": map[string]interface{}{ key: map[string]interface{}{ "query": val, "operator": "and", "fuzziness": "AUTO", }, }, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) continue } // ===== 默认前缀匹配 ===== cond := map[string]interface{}{ "prefix": map[string]interface{}{key: val}, } must = append(must, cond) fmt.Printf("[DEBUG] must += %v\n", cond) } // ========== 分页 ========== page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10"))) from := (page - 1) * pageSize // ========== pageSize大于500则根据id查询 ========= fmt.Printf("[DEBUG] page=%d pageSize=%d from=%d\n", page, pageSize, from) var sort []map[string]interface{} if 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 := map[string]interface{}{ "from": from, "size": pageSize, "query": map[string]interface{}{ "bool": map[string]interface{}{ "must": must, }, }, "sort": sort, "_source": map[string]interface{}{ "includes": []string{"isbn", "author", "book_name", "fix_price", "publisher"}, }, } body, _ := json.MarshalIndent(query, "", " ") 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() monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint) queryBody := string(body) req := esapi.SearchRequest{ Index: []string{ESIndex}, Body: bytes.NewReader([]byte(queryBody)), TrackTotalHits: true, Pretty: true, } res, duration, err := monitoredES.Search(context.Background(), &req) fmt.Printf("[DEBUG] ES Query Response:\n%s\n", res) fmt.Printf("[DEBUG] ES 查询耗时:%dms\n", duration.Milliseconds()) if err != nil { fmt.Printf("[ERROR] ES.Client.Search error: %v\n", err) return nil, 0, err } defer res.Body.Close() // 读取响应 var buf bytes.Buffer // 使用CopyBuffer可以重用缓冲区 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() //rawData, err := io.ReadAll(res.Body) 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([]ESBook, 0, len(parsed.Hits.Hits)) for _, hit := range parsed.Hits.Hits { list = append(list, hit.Source) } return list, parsed.Hits.Total.Value, nil } // Gin Handler 包装器 func (svc *ESSearchService) SearchBookBaseInfoESHandler(c *gin.Context) { // DEBUG: 打印所有 query 参数 fmt.Printf("[DEBUG] Query Params: %v\n", c.Request.URL.RawQuery) list, total, err := svc.SearchBookBaseInfoES(c) if err != nil { fmt.Printf("[ERROR] SearchBookBaseInfoES failed: %v\n", err) c.JSON(500, gin.H{"error": err.Error()}) return } // 转换为Java兼容的响应格式 responseList := make([]ESBookResponse, 0, len(list)) for _, book := range list { responseList = append(responseList, book.ConvertToResponse()) } // 分页 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSizeStr := c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10")) pageSize, _ := strconv.Atoi(pageSizeStr) // DEBUG: 打印响应数据摘要 fmt.Printf("[DEBUG] Response Info => total=%d page=%d pageSize=%d returnCount=%d\n", total, page, pageSize, len(responseList)) c.JSON(200, gin.H{ "current_page": page, "data": responseList, "per_page": pageSize, "total": total, }) } func (svc *ESSearchService) BatchGetBookBaseInfoESHandler(c *gin.Context) { list, total, err := svc.BatchGetBookBaseInfoES(c) if err != nil { fmt.Printf("[ERROR] BatchGetBookBaseInfoES failed: %v\n", err) c.JSON(500, gin.H{"error": err.Error()}) return } isbnList := make([]map[string]interface{}, 0, len(list)) for _, book := range list { isbnList = append(isbnList, map[string]interface{}{ "isbn": book.ISBN, "book_name": book.BookName.Value, "fix_price": float64(book.FixPrice), "author": book.Author, "publisher": book.Publisher, }) } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSizeStr := c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10")) pageSize, _ := strconv.Atoi(pageSizeStr) fmt.Printf("[DEBUG] Response Info => total=%d page=%d pageSize=%d returnCount=%d\n", total, page, pageSize, len(isbnList)) c.JSON(200, gin.H{ "current_page": page, "data": isbnList, "per_page": pageSize, "total": total, }) } func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) { isbn := c.Query("isbn") if isbn == "" { log.Printf("[SearchBooksHandler] 缺少 isbn 参数") c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) return } log.Printf("[SearchBooksHandler] ISBN 模糊搜索:%s", isbn) ctx := context.Background() endpoint := c.FullPath() // 获取监控的 Redis 客户端 db1Client, err := redisClient.GetClientByName("db1") if err == nil { monitoredRedis := monitor.NewMonitoredRedisClient(db1Client, endpoint) val, _, err := monitoredRedis.Get(ctx, isbn) if err == nil && val != "" { log.Printf("[SearchBooksHandler] 从 Redis db1 查询到数据:%s", isbn) var redisBook request.BookInfo if err := json.Unmarshal([]byte(val), &redisBook); err == nil { esBook := ConvertRedisBookToESBook(&redisBook) if esBook != nil { responseData := esBook.ConvertToResponse() c.JSON(200, gin.H{ "data": responseData, }) return } } } } // ES 查询 result, err := svc.SearchBooks(isbn, endpoint) if err != nil { c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()}) return } responseList := make([]ESBookResponse, 0, len(result)) for _, book := range result { responseList = append(responseList, book.ConvertToResponse()) } c.JSON(200, gin.H{ "count": len(result), "data": responseList, }) } func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) { isbn := c.Query("isbn") if isbn == "" { log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数") c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) return } log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn) ctx := context.Background() endpoint := c.FullPath() // Redis 查询(使用监控) db1Client, err := redisClient.GetClientByName("db1") if err == nil { monitoredRedis := monitor.NewMonitoredRedisClient(db1Client, endpoint) val, _, err := monitoredRedis.Get(ctx, isbn) if err == nil && val != "" { log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据:%s", isbn) var redisBook request.BookInfo if err := json.Unmarshal([]byte(val), &redisBook); err == nil { esBook := ConvertRedisBookToESBook(&redisBook) if esBook != nil { responseData := esBook.ConvertToResponse() c.JSON(200, gin.H{ "data": responseData, }) return } } else { log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err) } } } // ES 查询(使用监控) query := map[string]interface{}{ "query": map[string]interface{}{ "term": map[string]interface{}{ "isbn": isbn, }, }, "_source": true, } body, err := json.Marshal(query) if err != nil { log.Printf("[SearchBookByISBNHandler] 构建查询 JSON 失败:%v", err) c.JSON(500, gin.H{"error": "构建查询失败"}) return } req := esapi.SearchRequest{ Index: []string{ESIndex}, Body: bytes.NewReader(body), TrackTotalHits: true, Pretty: true, } // 创建监控客户端并执行查询 monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint) resp, duration, err := monitoredES.Search(ctx, &req) log.Printf("[SearchBookByISBNHandler] ES 查询耗时:%dms", duration.Milliseconds()) if err != nil { log.Printf("[SearchBookByISBNHandler] ES 查询失败:%v", err) c.JSON(500, gin.H{"error": "ES 查询失败:" + err.Error()}) return } defer resp.Body.Close() if resp.IsError() { log.Printf("[SearchBookByISBNHandler] ES 返回错误:%s", resp.String()) c.JSON(500, gin.H{"error": "ES 返回错误:" + resp.String()}) return } var parsed esHitsWrapper if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { log.Printf("[SearchBookByISBNHandler] 解析 ES 响应失败:%v", err) c.JSON(500, gin.H{"error": "解析响应失败"}) return } var result *ESBook if len(parsed.Hits.Hits) > 0 { result = &parsed.Hits.Hits[0].Source } if result == nil { log.Printf("[SearchBookByISBNHandler] ES 中未找到 ISBN: %s,从孔夫子抓取", isbn) apiBook, err := kongfz.GetBookImageByISBN(isbn, "CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f") if err != nil { log.Printf("[SearchBookByISBNHandler] 孔夫子 API 查询失败:%v", err) c.JSON(500, gin.H{"error": err.Error()}) return } if apiBook == nil || apiBook.Data.ISBN == "" { log.Printf("[SearchBookByISBNHandler] 孔夫子 API 未找到图书信息 ISBN: %s", isbn) c.JSON(404, gin.H{"error": "未找到图书信息"}) return } log.Printf("[SearchBookByISBNHandler] 获取到图书信息:%+v", apiBook.Data) pddBookPicURL := "" if apiBook.Data.BookPic != "" { url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPic, isbn, "true", apiBook.Data.BookName, "true") if err != nil { log.Printf("[SearchBookByISBNHandler] 上传 book_pic 失败:%v", err) } else { pddBookPicURL = url log.Printf("[SearchBookByISBNHandler] 上传 book_pic 成功:%s", url) } } pddBookPicSURL := "" if apiBook.Data.BookPicS != "" { url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPicS, isbn, "true", apiBook.Data.BookName, "true") if err != nil { log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 失败:%v", err) } else { pddBookPicSURL = url log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 成功:%s", url) } } esBook := ConvertKongfzToESBook(apiBook) esBook.BookPicS.PddResponse = pddBookPicSURL esBook.BookPic.PddPath = pddBookPicURL result, err = svc.AddBookToES(c.Request.Context(), esBook) if err != nil { log.Printf("[SearchBookByISBNHandler] 写入 ES 失败:%v", err) c.JSON(500, gin.H{"error": err.Error()}) return } log.Printf("[SearchBookByISBNHandler] 写入 ES 成功,ISBN: %s", isbn) } responseData := result.ConvertToResponse() c.JSON(200, gin.H{ "data": responseData, }) } // func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) { // isbn := c.Query("isbn") // if isbn == "" { // c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) // return // } // // ctx := context.Background() // // db4Client, err := redisClient.GetClientByName("db1") // if err == nil { // val, err := db4Client.Get(ctx, isbn).Result() // if err == nil && val != "" { // log.Printf("[SearchBooksHandler] 从 Redis db1 查询到数据: %s", isbn) // // 使用 RedisBookInfo 结构体解析 // var redisBook request.BookInfo // if err := json.Unmarshal([]byte(val), &redisBook); err == nil { // // 转换为 ESBook // esBook := ConvertRedisBookToESBook(&redisBook) // if esBook != nil { // responseData := esBook.ConvertToResponse() // c.JSON(200, gin.H{ // "data": responseData, // }) // return // } // } else { // log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err) // } // } // } // // result, err := svc.SearchBooks(isbn) // if err != nil { // c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()}) // return // } // // responseList := make([]ESBookResponse, 0, len(result)) // for _, book := range result { // responseList = append(responseList, book.ConvertToResponse()) // } // // c.JSON(200, gin.H{ // "count": len(result), // "data": responseList, // }) // } //func (svc *ESSearchService) SearchBookByISBNHandler(c *gin.Context) { // // isbn := c.Query("isbn") // if isbn == "" { // log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数") // c.JSON(400, gin.H{"error": "缺少 isbn 参数"}) // return // } // // log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn) // // ctx := context.Background() // // db4Client, err := redisClient.GetClientByName("db1") // fmt.Println(db4Client) // // if err != nil { // log.Printf("[SearchBookByISBNHandler] 获取 Redis db1 客户端失败: %v", err) // } else { // val, err := db4Client.Get(ctx, isbn).Result() // if err == nil && val != "" { // log.Printf("[SearchBookByISBNHandler] 从 Redis db1 查询到数据: %s", isbn) // // 使用 RedisBookInfo 结构体解析 // var redisBook request.BookInfo // if err := json.Unmarshal([]byte(val), &redisBook); err == nil { // // 转换为 ESBook // esBook := ConvertRedisBookToESBook(&redisBook) // if esBook != nil { // responseData := esBook.ConvertToResponse() // c.JSON(200, gin.H{ // "data": responseData, // }) // return // } // } else { // log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败:%v", err) // } // } else { // log.Printf("[SearchBookByISBNHandler] Redis db1 中未找到 ISBN: %s", isbn) // } // } // // result, err := svc.SearchBookByISBN(isbn) // if err != nil { // log.Printf("[SearchBookByISBNHandler] ES 查询失败: %v", err) // c.JSON(500, gin.H{"error": err.Error()}) // return // } // // if result == nil { // log.Printf("[SearchBookByISBNHandler] ES 中未找到 ISBN: %s,从孔夫子抓取", isbn) // // apiBook, err := kongfz.GetBookImageByISBN(isbn, "CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f") // if err != nil { // log.Printf("[SearchBookByISBNHandler] 孔夫子 API 查询失败: %v", err) // c.JSON(500, gin.H{"error": err.Error()}) // return // } // if apiBook == nil || apiBook.Data.ISBN == "" { // log.Printf("[SearchBookByISBNHandler] 孔夫子 API 未找到图书信息 ISBN: %s", isbn) // c.JSON(404, gin.H{"error": "未找到图书信息"}) // return // } // // log.Printf("[SearchBookByISBNHandler] 获取到图书信息: %+v", apiBook.Data) // // pddBookPicURL := "" // if apiBook.Data.BookPic != "" { // url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPic, isbn, "true", apiBook.Data.BookName, "true") // if err != nil { // log.Printf("[SearchBookByISBNHandler] 上传 book_pic 失败: %v", err) // } else { // pddBookPicURL = url // log.Printf("[SearchBookByISBNHandler] 上传 book_pic 成功: %s", url) // } // } // // pddBookPicSURL := "" // if apiBook.Data.BookPicS != "" { // url, err := image.DownloadAndUploadBookImage(apiBook.Data.BookPicS, isbn, "true", apiBook.Data.BookName, "true") // if err != nil { // log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 失败: %v", err) // } else { // pddBookPicSURL = url // log.Printf("[SearchBookByISBNHandler] 上传 book_pic_s 成功: %s", url) // } // } // // esBook := ConvertKongfzToESBook(apiBook) // // esBook.BookPicS.PddResponse = pddBookPicSURL // esBook.BookPic.PddPath = pddBookPicURL // //log.Printf("[SearchBookByISBNHandler] 写入 ES: %+v", esBook) // // result, err = svc.AddBookToES(c.Request.Context(), esBook) // if err != nil { // log.Printf("[SearchBookByISBNHandler] 写入 ES 失败: %v", err) // c.JSON(500, gin.H{"error": err.Error()}) // return // } // // log.Printf("[SearchBookByISBNHandler] 写入 ES 成功, ISBN: %s", isbn) // } else { // //log.Printf("[SearchBookByISBNHandler] 从 ES 查询到图书: %+v", result) // } // // responseData := result.ConvertToResponse() // c.JSON(200, gin.H{ // "data": responseData, // }) //} // ConvertKongfzToESBook 将第三方接口返回的数据转换为 ESBook 结构 func ConvertKongfzToESBook(apiBook *kongfz.BookResponse) *ESBook { if apiBook == nil || apiBook.Data.ISBN == "" { return nil } pubTimeStr := "" if apiBook.Data.PublicationTime != 0 { pubTimeStr = time.Unix(apiBook.Data.PublicationTime, 0).Format("2006-01") } // 处理 fix_price -> 分为单位 fixPriceFen := 0 if apiBook.Data.FixPrice != "" { priceFloat, err := strconv.ParseFloat(apiBook.Data.FixPrice, 64) if err == nil { fixPriceFen = int(priceFloat * 100) // 元 -> 分 } } return &ESBook{ ISBN: apiBook.Data.ISBN, BookName: FlexibleString{Value: apiBook.Data.BookName}, Author: apiBook.Data.Author, Publisher: apiBook.Data.Publisher, PublicationTime: pubTimeStr, FixPrice: Float64OrString(float64(fixPriceFen)), BookPic: BookPicObj{ LocalPath: "", PddPath: apiBook.Data.BookPic, // 初始化 BookPic 字段 }, BookPicS: BookPicSObj{ LocalPath: "", PddResponse: apiBook.Data.BookPicS, }, BookPicObjS: map[string]interface{}{ "pddResponse": apiBook.Data.BookPicS, }, // 其他字段默认赋值 BuyCounts: 0, SellCounts: 0, TotalSale: 0, BookPicW: make(map[string]interface{}), IsSuit: 0, Category: "", Content: "", } } func (svc *ESSearchService) SearchBookByBookNameHandler(c *gin.Context) { bookName := c.Query("bookName") if bookName == "" { c.JSON(400, gin.H{"error": "缺少 bookName 参数"}) return } result, err := svc.SearchBookByBookName(bookName) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } if len(result) == 0 { c.JSON(404, gin.H{"error": "未找到该书名的图书"}) return } // 转换为Java兼容的响应格式 responseList := make([]ESBookResponse, 0, len(result)) for _, book := range result { responseList = append(responseList, book.ConvertToResponse()) } c.JSON(200, gin.H{ "count": len(result), "data": responseList, }) } func (svc *ESSearchService) SearchBooksAllFieldsHandler(c *gin.Context) { keyword := c.Query("q") if keyword == "" { c.JSON(400, gin.H{"error": "缺少搜索参数 q"}) return } result, err := svc.SearchBooksAllFields(keyword) if err != nil { c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()}) return } // 转换为Java兼容的响应格式 responseList := make([]ESBookResponse, 0, len(result)) for _, book := range result { responseList = append(responseList, book.ConvertToResponse()) } c.JSON(200, gin.H{ "count": len(result), "data": responseList, }) } func (svc *ESSearchService) SearchBooksByConditionsHandler(c *gin.Context) { conds := map[string]string{ "book_name": c.Query("book_name"), "isbn": c.Query("isbn"), "author": c.Query("author"), "category": c.Query("category"), "publisher": c.Query("publisher"), "publication_time": c.Query("publication_time"), "day_sale_30": c.Query("day_sale_30"), } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) result, total, err := svc.SearchBooksByConditions(conds, page, pageSize) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } // 转换为Java兼容的响应格式 responseList := make([]ESBookResponse, 0, len(result)) for _, book := range result { responseList = append(responseList, book.ConvertToResponse()) } c.JSON(200, gin.H{ "count": len(result), "total": total, "data": responseList, "page": page, "pageSize": pageSize, }) } // CountByIDRangeHandler Gin 包装器 func (svc *ESSearchService) CountByIDRangeHandler(c *gin.Context) { minID, _ := strconv.Atoi(c.DefaultQuery("minID", "0")) maxID, _ := strconv.Atoi(c.DefaultQuery("maxID", "0")) if minID == 0 || maxID == 0 { c.JSON(400, gin.H{ "error": "缺少 minID 或 maxID 参数", }) return } count, err := svc.CountByIDRange(minID, maxID) if err != nil { c.JSON(500, gin.H{ "error": "ES 查询失败", "details": err.Error(), }) return } c.JSON(200, gin.H{ "minID": minID, "maxID": maxID, "count": count, }) } // CheckBookSuit 检查书名是否包含套装关键字 func CheckBookSuit(bookName string) bool { if bookName == "" { return false } // 套装关键字列表 suitKeywords := []string{ "套装", "卷本", "上中下", "上、下", "共二", "共三", "共四", "共五", "共六", "共七", "共八", "共九", "共十", "共十一", "共十二", "共十三", "共十四", "共十五", "共十六", "共十七", "共十八", "共十九", "共二十", "共二十一", "共二十二", "共二十三", "共二十四", "共二十五", "共二十六", "共二十七", "共二十八", "共二十九", "共三十", "共2", "共3", "共4", "共5", "共6", "共7", "共8", "共9", "全2", "全3", "全4", "全5", "全6", "全7", "全八", "全9", "共11", "共12", "共13", "共14", "共15", "共16", "共17", "共18", "共19", "全二", "全三", "全四", "全五", "全六", "全七", "全八", "全九", "2本合售", "3本合售", "4本合售", "5本合售", "6本合售", "7本合售", "8本合售", "9本合售", "合二", "合三", "合四", "合五", "合六", "合七", "合八", "合九", "合十", "合十一", "合十二", "合十三", "合十四", "合十五", "合十六", "合十七", "合十八", "合十九", "全套", "全套上下", "一套两本", "一套三本", "一套四本", "一套五本", "一套六本", "一套七本", "一套八本", "一套九本", "一套十本", "一套十一本", "1-1", "1-2", "1-3", "1-4", "1-5", "1-6", "1-7", "1-8", "1-9", "一-一", "一-二", "一-三", "一-四", "一-五", "一-六", "一-七", "一-八", "一-九", "壹-壹", "壹-贰", "壹-叁", "壹-肆", "壹-伍", "壹-陆", "壹-柒", "壹-捌", "壹-玖", "共十一", "共十二", "共十三", "共十四", "共十五", "共十六", "共十七", "共十八", "共十九", "全十一", "全十二", "全十三", "全十四", "全十五", "全十六", "全十七", "全十八", "全十九", "共贰", "共叁", "共肆", "共伍", "共陆", "共柒", "共捌", "共玖", "共拾", "全贰", "全叁", "全肆", "全伍", "全陆", "全柒", "全捌", "全玖", "全拾", "合贰", "合叁", "合肆", "合伍", "合陆", "合柒", "合捌", "合玖", "合拾", "共拾壹", "共拾贰", "共拾叁", "共拾肆", "共拾伍", "共拾陆", "共拾柒", "共拾捌", "共拾玖", "全拾壹", "全拾贰", "全拾叁", "全拾肆", "全拾伍", "全拾陆", "全拾柒", "全拾捌", "全拾玖", "合拾壹", "合拾贰", "合拾叁", "合拾肆", "合拾伍", "合拾陆", "合拾柒", "合拾捌", "合拾玖", "合2本", "合3本", "合4本", "合5本", "合6本", "合7本", "合8本", "合9本", "合10本", "合11本", "合12本", "合13本", "合14本", "合15本", "合16本", "合17本", "合18本", "合19本", "共2本", "共3本", "共4本", "共5本", "共6本", "共7本", "共8本", "共9本", "共10本", "共11本", "共12本", "共13本", "共14本", "共15本", "共16本", "共17本", "共18本", "共19本", "全2本", "全3本", "全4本", "全5本", "全6本", "全7本", "全8本", "全9本", "全10本", "全11本", "全12本", "全13本", "全14本", "全15本", "全16本", "全17本", "全18本", "全19本", "合二本", "合三本", "合四本", "合五本", "合六本", "合七本", "合八本", "合九本", "合十本", "合十一本", "合十二本", "合十三本", "合十四本", "合十五本", "合十六本", "合十七本", "合十八本", "合十九本", "共二本", "共三本", "共四本", "共五本", "共六本", "共七本", "共八本", "共九本", "共十本", "共十一本", "共十二本", "共十三本", "共十四本", "共十五本", "共十六本", "共十七本", "共十八本", "共十九本", "全二本", "全三本", "全四本", "全五本", "全六本", "全七本", "全八本", "全九本", "全十本", "全十一本", "全十二本", "全十三本", "全十四本", "全十五本", "全十六本", "全十七本", "全十八本", "全十九本", "合贰本", "合叁本", "合肆本", "合伍本", "合陆本", "合柒本", "合捌本", "合玖本", "合拾本", "合拾壹本", "合拾贰本", "合拾叁本", "合拾肆本", "合拾伍本", "合拾陆本", "合拾柒本", "合拾捌本", "合拾玖本", "共贰本", "共叁本", "共肆本", "共伍本", "共陆本", "共柒本", "共捌本", "共玖本", "共拾本", "共拾壹本", "共拾贰本", "共拾叁本", "共拾肆本", "共拾伍本", "共拾陆本", "共拾柒本", "共拾捌本", "共拾玖本", "全贰本", "全叁本", "全肆本", "全伍本", "全陆本", "全柒本", "全捌本", "全玖本", "全拾本", "全拾壹本", "全拾贰本", "全拾叁本", "全拾肆本", "全拾伍本", "全拾陆本", "全拾柒本", "全拾捌本", "全拾玖本", "合2册", "合3册", "合4册", "合5册", "合6册", "合7册", "合8册", "合9册", "合10册", "合11册", "合12册", "合13册", "合14册", "合15册", "合16册", "合17册", "合18册", "合19册", "共2册", "共3册", "共4册", "共5册", "共6册", "共7册", "共8册", "共9册", "共10册", "共11册", "共12册", "共13册", "共14册", "共15册", "共16册", "共17册", "共18册", "共19册", "全2册", "全3册", "全4册", "全5册", "全6册", "全7册", "全8册", "全9册", "全10册", "全11册", "全12册", "全13册", "全14册", "全15册", "全16册", "全17册", "全18册", "全19册", "合二册", "合三册", "合四册", "合五册", "合六册", "合七册", "合八册", "合九册", "合十册", "合十一册", "合十二册", "合十三册", "合十四册", "合十五册", "合十六册", "合十七册", "合十八册", "合十九册", "共二册", "共三册", "共四册", "共五册", "共六册", "共七册", "共八册", "共九册", "共十册", "共十一册", "共十二册", "共十三册", "共十四册", "共十五册", "共十六册", "共十七册", "共十八册", "共十九册", "全二册", "全三册", "全四册", "全五册", "全六册", "全七册", "全八册", "全九册", "全十册", "全十一册", "全十二册", "全十三册", "全十四册", "全十五册", "全十六册", "全十七册", "全十八册", "全十九册", "合贰册", "合叁册", "合肆册", "合伍册", "合陆册", "合柒册", "合捌册", "合玖册", "合拾册", "合拾壹册", "合拾贰册", "合拾叁册", "合拾肆册", "合拾伍册", "合拾陆册", "合拾柒册", "合拾捌册", "合拾玖册", "共贰册", "共叁册", "共肆册", "共伍册", "共陆册", "共柒册", "共捌册", "共玖册", "共拾册", "共拾壹册", "共拾贰册", "共拾叁册", "共拾肆册", "共拾伍册", "共拾陆册", "共拾柒册", "共拾捌册", "共拾玖册", "全贰册", "全叁册", "全肆册", "全伍册", "全陆册", "全柒册", "全捌册", "全玖册", "全拾册", "全拾壹册", "全拾贰册", "全拾叁册", "全拾肆册", "全拾伍册", "全拾陆册", "全拾柒册", "全拾捌册", "全拾玖册", "合2卷", "合3卷", "合4卷", "合5卷", "合6卷", "合7卷", "合8卷", "合9卷", "合10卷", "合11卷", "合12卷", "合13卷", "合14卷", "合15卷", "合16卷", "合17卷", "合18卷", "合19卷", "共2卷", "共3卷", "共4卷", "共5卷", "共6卷", "共7卷", "共8卷", "共9卷", "共10卷", "共11卷", "共12卷", "共13卷", "共14卷", "共15卷", "共16卷", "共17卷", "共18卷", "共19卷", "全2卷", "全3卷", "全4卷", "全5卷", "全6卷", "全7卷", "全8卷", "全9卷", "全10卷", "全11卷", "全12卷", "全13卷", "全14卷", "全15卷", "全16卷", "全17卷", "全18卷", "全19卷", "合二卷", "合三卷", "合四卷", "合五卷", "合六卷", "合七卷", "合八卷", "合九卷", "合十卷", "合十一卷", "合十二卷", "合十三卷", "合十四卷", "合十五卷", "合十六卷", "合十七卷", "合十八卷", "合十九卷", "共二卷", "共三卷", "共四卷", "共五卷", "共六卷", "共七卷", "共八卷", "共九卷", "共十卷", "共十一卷", "共十二卷", "共十三卷", "共十四卷", "共十五卷", "共十六卷", "共十七卷", "共十八卷", "共十九卷", "全二卷", "全三卷", "全四卷", "全五卷", "全六卷", "全七卷", "全八卷", "全九卷", "全十卷", "全十一卷", "全十二卷", "全十三卷", "全十四卷", "全十五卷", "全十六卷", "全十七卷", "全十八卷", "全十九卷", "合贰卷", "合叁卷", "合肆卷", "合伍卷", "合陆卷", "合柒卷", "合捌卷", "合玖卷", "合拾卷", "合拾壹卷", "合拾贰卷", "合拾叁卷", "合拾肆卷", "合拾伍卷", "合拾陆卷", "合拾柒卷", "合拾捌卷", "合拾玖卷", "共贰卷", "共叁卷", "共肆卷", "共伍卷", "共陆卷", "共柒卷", "共捌卷", "共玖卷", "共拾卷", "共拾壹卷", "共拾贰卷", "共拾叁卷", "共拾肆卷", "共拾伍卷", "共拾陆卷", "共拾柒卷", "共拾捌卷", "共拾玖卷", "全贰卷", "全叁卷", "全肆卷", "全伍卷", "全陆卷", "全柒卷", "全捌卷", "全玖卷", "全拾卷", "全拾壹卷", "全拾贰卷", "全拾叁卷", "全拾肆卷", "全拾伍卷", "全拾陆卷", "全拾柒卷", "全拾捌卷", "全拾玖卷", "合2辑", "合3辑", "合4辑", "合5辑", "合6辑", "合7辑", "合8辑", "合9辑", "合10辑", "合11辑", "合12辑", "合13辑", "合14辑", "合15辑", "合16辑", "合17辑", "合18辑", "合19辑", "共2辑", "共3辑", "共4辑", "共5辑", "共6辑", "共7辑", "共8辑", "共9辑", "共10辑", "共11辑", "共12辑", "共13辑", "共14辑", "共15辑", "共16辑", "共17辑", "共18辑", "共19辑", "全2辑", "全3辑", "全4辑", "全5辑", "全6辑", "全7辑", "全8辑", "全9辑", "全10辑", "全11辑", "全12辑", "全13辑", "全14辑", "全15辑", "全16辑", "全17辑", "全18辑", "全19辑", "合二辑", "合三辑", "合四辑", "合五辑", "合六辑", "合七辑", "合八辑", "合九辑", "合十辑", "合十一辑", "合十二辑", "合十三辑", "合十四辑", "合十五辑", "合十六辑", "合十七辑", "合十八辑", "合十九辑", "共二辑", "共三辑", "共四辑", "共五辑", "共六辑", "共七辑", "共八辑", "共九辑", "共十辑", "共十一辑", "共十二辑", "共十三辑", "共十四辑", "共十五辑", "共十六辑", "共十七辑", "共十八辑", "共十九辑", "全二辑", "全三辑", "全四辑", "全五辑", "全六辑", "全七辑", "全八辑", "全九辑", "全十辑", "全十一辑", "全十二辑", "全十三辑", "全十四辑", "全十五辑", "全十六辑", "全十七辑", "全十八辑", "全十九辑", "合贰辑", "合叁辑", "合肆辑", "合伍辑", "合陆辑", "合柒辑", "合捌辑", "合玖辑", "合拾辑", "合拾壹辑", "合拾贰辑", "合拾叁辑", "合拾肆辑", "合拾伍辑", "合拾陆辑", "合拾柒辑", "合拾捌辑", "合拾玖辑", "共贰辑", "共叁辑", "共肆辑", "共伍辑", "共陆辑", "共柒辑", "共捌辑", "共玖辑", "共拾辑", "共拾壹辑", "共拾贰辑", "共拾叁辑", "共拾肆辑", "共拾伍辑", "共拾陆辑", "共拾柒辑", "共拾捌辑", "共拾玖辑", "全贰辑", "全叁辑", "全肆辑", "全伍辑", "全陆辑", "全柒辑", "全捌辑", "全玖辑", "全拾辑", "全拾壹辑", "全拾贰辑", "全拾叁辑", "全拾肆辑", "全拾伍辑", "全拾陆辑", "全拾柒辑", "全拾捌辑", "全拾玖辑", "一函二", "一函三", "一函四", "一函五", "一函六", "一函七", "一函八", "一函九", "一函十", } // 检查书名是否包含任何一个套装关键字 for _, keyword := range suitKeywords { if strings.Contains(bookName, keyword) { return true } } return false } // CheckBookSuitHandler 检查书名是否包含套装关键字的HTTP处理器 func (svc *ESSearchService) CheckBookSuitHandler(c *gin.Context) { bookName := c.Query("bookName") if bookName == "" { c.JSON(400, gin.H{ "error": "缺少 bookName 参数", }) return } isSuit := CheckBookSuit(bookName) c.JSON(200, gin.H{ "code": 200, "message": "success", "data": gin.H{ "book_name": bookName, "is_suit": isSuit, }, }) } // UpdateBookSuitByISBNHandler 根据ISBN更新图书的is_suit字段 func (svc *ESSearchService) UpdateBookSuitByISBNHandler(c *gin.Context) { // 获取请求参数 var request struct { ISBN string `json:"isbn" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(400, gin.H{ "error": "请求参数错误", "details": err.Error(), }) return } isbn := strings.TrimSpace(request.ISBN) if isbn == "" { c.JSON(400, gin.H{ "error": "ISBN不能为空", }) return } // 根据ISBN查询图书 book, err := svc.SearchBookByISBN(isbn) if err != nil { c.JSON(500, gin.H{ "error": "查询图书失败", "details": err.Error(), }) return } if book == nil { c.JSON(404, gin.H{ "error": "未找到该ISBN对应的图书", }) return } // 检查书名是否包含套装关键字 isSuit := 0 if CheckBookSuit(book.BookName.Value) { isSuit = 1 log.Printf("书名 '%s' 包含套装关键字,is_suit设置为1", book.BookName.Value) } else { log.Printf("书名 '%s' 不包含套装关键字,is_suit设置为0", book.BookName.Value) } // 更新ES中的is_suit字段 body := map[string]interface{}{ "script": map[string]interface{}{ "source": "ctx._source.is_suit = params.is_suit;", "lang": "painless", "params": map[string]interface{}{ "is_suit": isSuit, }, }, "query": map[string]interface{}{ "term": map[string]interface{}{ "isbn": isbn, }, }, } fmt.Println(body) payload, _ := json.Marshal(body) res, err := svc.ES.Client.UpdateByQuery( []string{ESIndex}, svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)), svc.ES.Client.UpdateByQuery.WithRefresh(true), svc.ES.Client.UpdateByQuery.WithConflicts("proceed"), ) if err != nil { c.JSON(500, gin.H{ "error": "更新ES失败", "details": err.Error(), }) return } defer res.Body.Close() if res.IsError() { c.JSON(500, gin.H{ "error": "ES返回错误", "details": res.String(), }) return } // 解析更新结果 var parsed struct { Total int `json:"total"` Updated int `json:"updated"` VersionConflicts int `json:"version_conflicts"` } if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil { c.JSON(500, gin.H{ "error": "解析更新结果失败", "details": err.Error(), }) return } // 返回结果 c.JSON(200, gin.H{ "code": 200, "message": "success", "data": gin.H{ "isbn": isbn, "book_name": book.BookName.Value, "is_suit": isSuit, "updated": parsed.Updated, "contains_suit_keyword": isSuit == 1, }, }) } // UpdateBookFieldsByISBNHandler 根据ISBN通用更新图书字段 func (svc *ESSearchService) UpdateBookFieldsByISBNHandler(c *gin.Context) { // 请求参数 var request struct { ISBN string `json:"isbn" binding:"required"` Data map[string]interface{} `json:"data" binding:"required"` } // 参数解析 if err := c.ShouldBindJSON(&request); err != nil { c.JSON(400, gin.H{ "error": "请求参数错误", "details": err.Error(), }) return } isbn := strings.TrimSpace(request.ISBN) if isbn == "" { c.JSON(400, gin.H{ "error": "ISBN不能为空", }) return } if len(request.Data) == 0 { c.JSON(400, gin.H{ "error": "至少提供一个要更新的字段", }) return } // 先确认 ISBN 是否存在 book, err := svc.SearchBookByISBN(isbn) fmt.Println("book:", book) if err != nil { c.JSON(500, gin.H{ "error": "查询ES失败", "details": err.Error(), }) return } if book == nil { c.JSON(404, gin.H{ "error": "未找到该ISBN对应的图书", }) return } // 调用 Service 层更新逻辑 updated, err := svc.UpdateBookFieldsByISBN( c.Request.Context(), isbn, request.Data, ) if err != nil { c.JSON(500, gin.H{ "error": "更新ES失败", "details": err.Error(), }) return } // 返回结果 c.JSON(200, gin.H{ "code": 200, "message": "success", "data": gin.H{ "isbn": isbn, "updated": updated, "fields_updated": len(request.Data), "updated_fields": func() []string { fields := make([]string, 0, len(request.Data)) for k := range request.Data { fields = append(fields, k) } return fields }(), }, }) } func (svc *ESSearchService) UpdateBookFieldsByISBN( ctx context.Context, isbn string, data map[string]interface{}, ) (int, error) { if isbn == "" { return 0, fmt.Errorf("ISBN不能为空") } if len(data) == 0 { return 0, fmt.Errorf("至少提供一个要更新的字段") } // 构建更新脚本 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, } for field, value := range data { if !allowedFields[field] { continue } scriptParts = append(scriptParts, fmt.Sprintf("ctx._source.%s = params.%s;", field, field)) params[field] = value } fmt.Println("scriptParts:", scriptParts) if len(scriptParts) == 0 { return 0, 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": isbn, }, }, } payload, _ := json.Marshal(body) res, err := svc.ES.Client.UpdateByQuery( []string{ESIndex}, svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)), svc.ES.Client.UpdateByQuery.WithRefresh(true), svc.ES.Client.UpdateByQuery.WithConflicts("proceed"), ) if err != nil { return 0, err } 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()) } var parsed struct { Updated int `json:"updated"` } _ = json.NewDecoder(res.Body).Decode(&parsed) return parsed.Updated, nil } // AddBookToESHandler 根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新 func (svc *ESSearchService) AddBookToESHandler(c *gin.Context) { var req 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 } // 打印接收参数 log.Printf("接收参数: %+v", req) // ====== 外层:先查 ES 是否存在 ====== book, err := svc.SearchBookByISBN(req.ISBN) if err != nil { c.JSON(500, gin.H{ "error": "查询ES失败", "msg": err.Error(), }) return } // ====== 已存在直接处理 ====== if book != nil { log.Printf("ES中已存在: %+v", book) updateData := make(map[string]interface{}) if book.Publisher == "" && req.Publisher != "" { updateData["publisher"] = req.Publisher } if book.PublicationTime == "" && req.PublicationTime != "" { updateData["publication_time"] = req.PublicationTime } if book.BookPic.PddPath == "" && req.BookPic.PddPath != "" { updateData["book_pic"] = map[string]interface{}{ "localPath": "", "pddPath": req.BookPic.PddPath, } } if book.BookPicS.PddResponse == "" && req.BookPicS.PddResponse != "" { updateData["book_pic_s"] = map[string]interface{}{ "localPath": "", "pddResponse": req.BookPicS.PddResponse, } } if book.BookName.Value == "" && req.BookName.Value != "" { updateData["book_name"] = req.BookName.Value } if book.Author == "" && req.Author != "" { updateData["author"] = req.Author } // 打印传递的参数 log.Printf("更新数据: %+v", updateData) if len(updateData) > 0 { updateData["update_time"] = fmt.Sprintf("%d", time.Now().Unix()) _, err := svc.UpdateBookFieldsByISBN( c.Request.Context(), req.ISBN, updateData, ) if err != nil { c.JSON(500, gin.H{ "error": "补全ES字段失败", "msg": err.Error(), }) return } } // 重新查询最新数据 newBook, _ := svc.SearchBookByISBN(req.ISBN) c.JSON(200, gin.H{ "data": newBook.ConvertToResponse(), "source": "es", }) return } // ====== 不存在,才真正新增 ====== newBook, err := svc.AddBookToES(c.Request.Context(), &req) if err != nil { c.JSON(500, gin.H{ "error": err.Error(), }) return } c.JSON(200, gin.H{ "data": newBook.ConvertToResponse(), "source": "service", }) } // AddBookFullToESHandler 新的完整插入接口,支持所有字段,book_name强制为string类型 func (svc *ESSearchService) AddBookFullToESHandler(c *gin.Context) { var req AddBookFullRequest // 参数校验 if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "参数解析错误", "message": err.Error(), }) return } // 强制校验 book_name 为字符串类型 //if req.BookName == "" { // c.JSON(http.StatusBadRequest, gin.H{ // "error": "book_name 不能为空", // "message": "book_name 字段必须为非空字符串", // }) // return //} // ISBN 必填校验 if req.ISBN == "" { c.JSON(http.StatusBadRequest, gin.H{ "error": "ISBN 不能为空", "message": "ISBN 字段为必填项", }) return } // 验证 BookPic 对象格式 if req.BookPic != nil { if err := validateBookPicObj(req.BookPic, "book_pic"); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "book_pic 格式错误", "message": err.Error(), }) return } } // 验证 BookPicS 对象格式 if req.BookPicS != nil { if err := validateBookPicSObj(req.BookPicS, "book_pic_s"); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "book_pic_s 格式错误", "message": err.Error(), }) return } } // 验证 BookPicW 对象格式 if req.BookPicW != nil { if err := validateBookPicWObj(req.BookPicW, "book_pic_w"); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "book_pic_w 格式错误", "message": err.Error(), }) return } } // 验证 BookPicObj 对象格式 if req.BookPicObj != nil { if err := validateBookPicObj(req.BookPicObj, "book_pic_obj"); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "book_pic_obj 格式错误", "message": err.Error(), }) return } } // 验证 BookPicObjS 对象格式 if req.BookPicObjS != nil { if err := validateBookPicObjS(req.BookPicObjS, "book_pic_obj_s"); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "book_pic_obj_s 格式错误", "message": err.Error(), }) return } } // 验证数值字段范围 if req.FixPrice < 0 { c.JSON(http.StatusBadRequest, gin.H{ "error": "fix_price 不能为负数", "message": "fix_price 字段必须为非负数", }) return } if req.IsSuit < 0 || req.IsSuit > 1 { c.JSON(http.StatusBadRequest, gin.H{ "error": "is_suit 值错误", "message": "is_suit 字段必须为 0 或 1", }) return } // 验证销量字段不能为负数 salesFields := []struct { name string value int }{ {"day_sale_7", req.DaySale7}, {"day_sale_15", req.DaySale15}, {"day_sale_30", req.DaySale30}, {"day_sale_60", req.DaySale60}, {"day_sale_90", req.DaySale90}, {"day_sale_180", req.DaySale180}, {"day_sale_365", req.DaySale365}, {"this_year_sale", req.ThisYearSale}, {"last_year_sale", req.LastYearSale}, {"total_sale", req.TotalSale}, } for _, field := range salesFields { if field.value < 0 { c.JSON(http.StatusBadRequest, gin.H{ "error": fmt.Sprintf("%s 不能为负数", field.name), "message": fmt.Sprintf("%s 字段必须为非负数", field.name), }) return } } // 验证购买和售卖次数不能为负数 if req.BuyCounts < 0 { c.JSON(http.StatusBadRequest, gin.H{ "error": "buy_counts 不能为负数", "message": "buy_counts 字段必须为非负数", }) return } if req.SellCounts < 0 { c.JSON(http.StatusBadRequest, gin.H{ "error": "sell_counts 不能为负数", "message": "sell_counts 字段必须为非负数", }) return } // 验证字符串字段长度(防止过长导致存储问题) if len(req.BookName) > 500 { c.JSON(http.StatusBadRequest, gin.H{ "error": "book_name 过长", "message": "book_name 字段长度不能超过500个字符", }) return } if len(req.Author) > 200 { c.JSON(http.StatusBadRequest, gin.H{ "error": "author 过长", "message": "author 字段长度不能超过200个字符", }) return } if len(req.Publisher) > 200 { c.JSON(http.StatusBadRequest, gin.H{ "error": "publisher 过长", "message": "publisher 字段长度不能超过200个字符", }) return } if len(req.ISBN) > 20 { c.JSON(http.StatusBadRequest, gin.H{ "error": "isbn 过长", "message": "isbn 字段长度不能超过20个字符", }) return } // 验证ISBN格式(简单验证) if req.ISBN != "" { // 移除连字符和空格后检查是否只包含数字 cleanISBN := strings.ReplaceAll(strings.ReplaceAll(req.ISBN, "-", ""), " ", "") if len(cleanISBN) < 10 || len(cleanISBN) > 13 { c.JSON(http.StatusBadRequest, gin.H{ "error": "isbn 格式错误", "message": "isbn 格式不正确,应为10-13位数字", }) return } // 检查是否只包含数字 for _, r := range cleanISBN { if r < '0' || r > '9' { c.JSON(http.StatusBadRequest, gin.H{ "error": "isbn 格式错误", "message": "isbn 只能包含数字", }) return } } } // 转换为 ESBook 结构体 esBook := &ESBook{ BookName: FlexibleString{Value: req.BookName}, // 强制使用字符串 ISBN: req.ISBN, Author: req.Author, Category: req.Category, Publisher: req.Publisher, PublicationTime: req.PublicationTime, BindingLayout: req.BindingLayout, FixPrice: Float64OrString(req.FixPrice), Content: req.Content, IsSuit: req.IsSuit, DaySale7: req.DaySale7, DaySale15: req.DaySale15, DaySale30: req.DaySale30, DaySale60: req.DaySale60, DaySale90: req.DaySale90, DaySale180: req.DaySale180, DaySale365: req.DaySale365, ThisYearSale: req.ThisYearSale, LastYearSale: req.LastYearSale, TotalSale: req.TotalSale, BuyCounts: req.BuyCounts, SellCounts: req.SellCounts, BookPicObj: req.BookPicObj, BookPicObjS: req.BookPicObjS, IsIllegal: req.IsIllegal, IsReturn: req.IsReturn, IsFilter: req.IsFilter, } // 处理复杂图片字段(已经过验证,直接使用) // BookPic if req.BookPic != nil { localPath := req.BookPic["localPath"].(string) pddPath := req.BookPic["pddPath"].(string) esBook.BookPic = BookPicObj{ LocalPath: localPath, PddPath: pddPath, } } else { esBook.BookPic = BookPicObj{LocalPath: "", PddPath: ""} } // BookPicS if req.BookPicS != nil { localPath := req.BookPicS["localPath"].(string) pddResponse := req.BookPicS["pddResponse"].(string) esBook.BookPicS = BookPicSObj{ LocalPath: localPath, PddResponse: pddResponse, } } else { esBook.BookPicS = BookPicSObj{LocalPath: "", PddResponse: ""} } // BookPicB esBook.BookPicB = req.BookPicB // BookPicW if req.BookPicW != nil { esBook.BookPicW = req.BookPicW } else { esBook.BookPicW = make(map[string]interface{}) } // 调用 AddBookToES 方法插入数据 book, err := svc.AddBookToES(c.Request.Context(), esBook) if err != nil { c.JSON(500, gin.H{ "error": "插入ES失败", "message": err.Error(), }) return } // 转换为Java兼容的响应格式 responseData := book.ConvertToResponse() c.JSON(http.StatusOK, gin.H{ "code": 200, "message": "success", "data": responseData, "source": "full_insert", }) } // AddBookToES 新增数据到es中 或者更新 func (svc *ESSearchService) AddBookToES(ctx context.Context, req *ESBook) (*ESBook, error) { // 打印入参 log.Printf("入参: %+v", req) if req.ISBN == "" { return nil, fmt.Errorf("ISBN不能为空") } now := time.Now().Unix() // 解析 book_pic_new bookPicObjS := map[string]interface{}{ "pddResponse": req.BookPicS.PddResponse, } // 获取销量数据 salesData, _ := tail.CheckSales([]string{req.ISBN}) log.Printf("销量数据: %+v", salesData) var daySale7, daySale15, daySale30, daySale60, daySale90, daySale180, daySale365 int var thisYearSale, lastYearSale, totalSale int if salesData != nil && salesData.Data != nil { if s, ok := salesData.Data[req.ISBN]; ok { parse := func(str string) int { if v, err := strconv.Atoi(str); err == nil { return v } return 0 } 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) } } if totalSale == 0 && req.BuyCounts > 0 { totalSale = int(req.BuyCounts) } // 套装判断 isSuit := 0 if CheckBookSuit(req.BookName.Value) { isSuit = 1 } // 生成新 ID 以便新增数据插入新ID lastID, err := svc.GetLastID() if err != nil { return nil, fmt.Errorf("获取最后ID失败: %w", err) } newID := lastID + 1 newBook := ESBook{ ID: int64(newID), BookName: FlexibleString{Value: req.BookName.Value}, BookPic: BookPicObj{ LocalPath: "", PddPath: req.BookPic.PddPath, }, BookPicS: BookPicSObj{ LocalPath: "", PddResponse: req.BookPicS.PddResponse, }, BookPicW: make(map[string]interface{}), ISBN: req.ISBN, Author: req.Author, Publisher: req.Publisher, PublicationTime: req.PublicationTime, FixPrice: req.FixPrice, IsSuit: isSuit, DaySale7: daySale7, DaySale15: daySale15, DaySale30: daySale30, DaySale60: daySale60, DaySale90: daySale90, DaySale180: daySale180, DaySale365: daySale365, ThisYearSale: thisYearSale, LastYearSale: lastYearSale, TotalSale: totalSale, BuyCounts: req.BuyCounts, SellCounts: req.SellCounts, BookPicObjS: bookPicObjS, UpdateTime: NumberOrString(fmt.Sprintf("%d", now)), IsIllegal: 0, IsReturn: 0, IsFilter: "000000", } // 打印 log.Printf("生成新数据: %+v", newBook) // 创建临时map用于序列化,避免FlexibleString的序列化问题 tempBook := map[string]interface{}{ "id": newBook.ID, "book_name": newBook.BookName.Value, // 直接使用字符串值 "book_pic": newBook.BookPic, "book_pic_s": newBook.BookPicS, "book_pic_b": newBook.BookPicB, "book_pic_w": newBook.BookPicW, "isbn": newBook.ISBN, "author": newBook.Author, "category": newBook.Category, "publisher": newBook.Publisher, "publication_time": newBook.PublicationTime, "binding_layout": newBook.BindingLayout, "fix_price": newBook.FixPrice, "content": newBook.Content, "is_suit": newBook.IsSuit, "day_sale_7": newBook.DaySale7, "day_sale_15": newBook.DaySale15, "day_sale_30": newBook.DaySale30, "day_sale_60": newBook.DaySale60, "day_sale_90": newBook.DaySale90, "day_sale_180": newBook.DaySale180, "day_sale_365": newBook.DaySale365, "this_year_sale": newBook.ThisYearSale, "last_year_sale": newBook.LastYearSale, "total_sale": newBook.TotalSale, "buy_counts": newBook.BuyCounts, "sell_counts": newBook.SellCounts, "book_pic_obj": newBook.BookPicObj, "book_pic_obj_s": newBook.BookPicObjS, "is_illegal": newBook.IsIllegal, "is_return": newBook.IsReturn, "is_filter": newBook.IsFilter, "update_time": newBook.UpdateTime, } log.Printf("[AddBookToES] 准备序列化tempBook的book_name: %s", tempBook["book_name"]) jsonData, err := json.Marshal(tempBook) if err != nil { return nil, fmt.Errorf("序列化失败: %w", err) } log.Printf("[AddBookToES] 序列化结果: %s", string(jsonData)) // 写入 ES esReq := esapi.IndexRequest{ Index: ESIndex, DocumentID: req.ISBN, Body: bytes.NewReader(jsonData), Refresh: "true", } log.Printf("写入 ES 请求: %+v", esReq) res, err := esReq.Do(ctx, svc.ES.Client.Transport) //log.Printf("写入 ES 响应: %+v", res) if err != nil { return nil, fmt.Errorf("ES写入失败: %w", err) } defer res.Body.Close() if res.IsError() { return nil, fmt.Errorf("ES返回错误: %s", res.String()) } return &newBook, nil } // GetLastID 查询最后一条文档的ID func (svc *ESSearchService) GetLastID() (int, error) { log.Println("[GetLastID] 开始查询最新文档 ID") query := `{ "size": 1, "sort": [{"id": {"order": "desc"}}] }` res, err := svc.ES.Client.Search( svc.ES.Client.Search.WithContext(context.Background()), svc.ES.Client.Search.WithIndex(ESIndex), svc.ES.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()) } // 打印日志 //log.Println("[GetLastID] ES 响应:", res.String()) //log.Println("[GetLastID] 响应内容:", res.Body) // 定义结构体,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 } // DeleteBookHandler 删除图书 func (svc *ESSearchService) DeleteBookHandler(c *gin.Context) { isbn := c.Query("isbn") if err := svc.DeleteBookByISBN(isbn); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "删除成功"}) } // DeleteBookByISBN 通过 ISBN 删除 ES 文档 // // func (svc *ESSearchService) DeleteBookByISBN(isbn string) error { // isbn = strings.TrimSpace(isbn) // log.Printf("[DeleteBookByISBN] 开始删除 | ISBN=%s", isbn) // // if isbn == "" { // log.Printf("[DeleteBookByISBN] ISBN 为空,取消删除") // return fmt.Errorf("ISBN 不能为空") // } // // // 查询文档,确认存在 // byISBN, err := svc.SearchBookByISBN(isbn) // if err != nil { // log.Printf("[DeleteBookByISBN] 查询 ISBN 出错: %v", err) // return err // } // if byISBN == nil { // log.Printf("[DeleteBookByISBN] 未找到 ISBN 对应的文档: %s", isbn) // return fmt.Errorf("未找到 ISBN 对应的文档: %s", isbn) // } // // log.Printf("[DeleteBookByISBN] 找到文档: %+v", byISBN) // // // 构建删除请求 // req := esapi.DeleteRequest{ // Index: ESIndex, // DocumentID: isbn, // 确认文档 ID 与 ISBN 一致,否则需要 DeleteByQuery // Refresh: "true", // } // // // 执行删除请求 // res, err := req.Do(context.Background(), svc.ES.Client.Transport) // if err != nil { // log.Printf("[DeleteBookByISBN] 执行 ES 删除请求失败: %v", err) // return fmt.Errorf("执行 ES 删除请求失败: %v", err) // } // defer res.Body.Close() // // // 检查响应状态 // if res.IsError() { // if res.StatusCode == 404 { // log.Printf("[DeleteBookByISBN] 文档不存在,已跳过删除 | ISBN=%s", isbn) // return nil // } // log.Printf("[DeleteBookByISBN] ES 返回错误: %s", res.String()) // return fmt.Errorf("ES 返回错误: %s", res.String()) // } // // // 解析响应结果 // var result struct { // Result string `json:"result"` // Version int `json:"_version"` // } // if err := json.NewDecoder(res.Body).Decode(&result); err != nil { // log.Printf("[DeleteBookByISBN] 解析 ES 删除响应失败: %v", err) // return fmt.Errorf("解析 ES 删除响应失败: %v", err) // } // // log.Printf("[DeleteBookByISBN] 成功删除 ES 文档 | ISBN=%s | result=%s | version=%d", isbn, result.Result, result.Version) // return nil // } func (svc *ESSearchService) DeleteBookByISBN(isbn string) error { isbn = strings.TrimSpace(isbn) log.Printf("[DeleteBookByISBN] 开始删除 | ISBN=%s", isbn) if isbn == "" { return fmt.Errorf("ISBN 不能为空") } //index := "books-from-mysql-v2" query := fmt.Sprintf(`{ "query": { "term": { "isbn": "%s" } } }`, isbn) res, err := svc.ES.Client.DeleteByQuery( []string{ESIndex}, strings.NewReader(query), svc.ES.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()) } log.Printf("[DeleteBookByISBN] 成功删除 ISBN=%s 对应的文档", isbn) return nil } // DeleteBookByID 通过 ID 删除 ES 文档 func (svc *ESSearchService) DeleteBookByID(id string) error { id = strings.TrimSpace(id) log.Printf("[DeleteBookByID] 开始删除 | ID=%s", id) if id == "" { return fmt.Errorf("ID 不能为空") } query := fmt.Sprintf(`{ "query": { "term": { "id": "%s" } } }`, id) res, err := svc.ES.Client.DeleteByQuery( []string{ESIndex}, strings.NewReader(query), svc.ES.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()) } log.Printf("[DeleteBookByID] 成功删除 ID=%s 对应的文档", id) return nil } // DeleteBookByIDHandler 根据ID删除图书的HTTP处理器 func (svc *ESSearchService) DeleteBookByIDHandler(c *gin.Context) { id := strings.TrimSpace(c.Query("id")) if id == "" { c.JSON(400, gin.H{ "error": "缺少 id 参数", }) return } if err := svc.DeleteBookByID(id); err != nil { c.JSON(500, gin.H{ "error": "删除失败", "details": err.Error(), }) return } c.JSON(200, gin.H{ "code": 200, "message": "success", "data": gin.H{ "id": id, "deleted": true, }, }) } // BatchAddBookRequest 批量插入请求结构体 type BatchAddBookRequest struct { Books []AddBookFullRequest `json:"books"` // 图书列表 } // BatchInsertResultItem 单个图书插入结果 type BatchInsertResultItem struct { Index int `json:"index"` // 在批次中的索引 ISBN string `json:"isbn"` // 图书ISBN Success bool `json:"success"` // 是否成功 Error string `json:"error"` // 错误信息(如果有) Document string `json:"document"` // 文档ID(如果成功) } // BatchInsertResult 批量插入结果 type BatchInsertResult struct { TotalCount int `json:"total_count"` // 总数量 SuccessCount int `json:"success_count"` // 成功数量 FailedCount int `json:"failed_count"` // 失败数量 Results []BatchInsertResultItem `json:"results"` // 详细结果 } // BatchAddBookToESHandler 批量插入图书到ES的HTTP处理器 func (svc *ESSearchService) BatchAddBookToESHandler(c *gin.Context) { var req BatchAddBookRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{ "error": "请求参数格式错误", "details": err.Error(), }) return } // 验证至少提供一本书 if len(req.Books) == 0 { c.JSON(400, gin.H{ "error": "至少提供一本要插入的图书", }) return } // 限制批量插入数量,避免一次性处理过多 maxBatchSize := 100 if len(req.Books) > maxBatchSize { c.JSON(400, gin.H{ "error": fmt.Sprintf("批量插入数量不能超过%d本", maxBatchSize), }) return } result := BatchInsertResult{ TotalCount: len(req.Books), SuccessCount: 0, FailedCount: 0, Results: make([]BatchInsertResultItem, 0, len(req.Books)), } // 逐个处理每本书 for i, book := range req.Books { item := BatchInsertResultItem{ Index: i, ISBN: book.ISBN, Success: false, } // 验证必填字段 if book.BookName == "" { item.Error = "book_name不能为空" result.Results = append(result.Results, item) result.FailedCount++ continue } if book.ISBN == "" { item.Error = "ISBN不能为空" result.Results = append(result.Results, item) result.FailedCount++ continue } // 验证ISBN格式 if !validateISBNFormat(book.ISBN) { item.Error = "ISBN格式不正确,应为10-13位数字" result.Results = append(result.Results, item) result.FailedCount++ continue } // 确保book_pic和book_pic_s存在且验证通过 if err := validateBookPicObj(book.BookPic, "book_pic"); err != nil { item.Error = fmt.Sprintf("book_pic验证失败: %v", err) result.Results = append(result.Results, item) result.FailedCount++ continue } if err := validateBookPicSObj(book.BookPicS, "book_pic_s"); err != nil { item.Error = fmt.Sprintf("book_pic_s验证失败: %v", err) result.Results = append(result.Results, item) result.FailedCount++ continue } if book.BookPicW != nil { if err := validateBookPicWObj(book.BookPicW, "book_pic_w"); err != nil { item.Error = fmt.Sprintf("book_pic_w验证失败: %v", err) result.Results = append(result.Results, item) result.FailedCount++ continue } } // 转换为ESBook结构体 esBook := &ESBook{ BookName: FlexibleString{Value: book.BookName}, // 强制使用字符串 ISBN: book.ISBN, Author: book.Author, Category: book.Category, Publisher: book.Publisher, PublicationTime: book.PublicationTime, BindingLayout: book.BindingLayout, FixPrice: Float64OrString(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, IsIllegal: book.IsIllegal, IsReturn: book.IsReturn, IsFilter: book.IsFilter, } // 处理复杂图片字段(已经过验证,直接使用) // BookPic - 验证函数确保了要么为nil,要么包含正确的字段 if book.BookPic != nil { localPath := book.BookPic["localPath"].(string) pddPath := book.BookPic["pddPath"].(string) esBook.BookPic = BookPicObj{ LocalPath: localPath, PddPath: pddPath, } } else { // 创建默认空对象 esBook.BookPic = BookPicObj{LocalPath: "", PddPath: ""} } // BookPicS - 验证函数确保了要么为nil,要么包含正确的字段 if book.BookPicS != nil { localPath := book.BookPicS["localPath"].(string) pddResponse := book.BookPicS["pddResponse"].(string) esBook.BookPicS = BookPicSObj{ LocalPath: localPath, PddResponse: pddResponse, } } else { // 创建默认空对象 esBook.BookPicS = BookPicSObj{LocalPath: "", PddResponse: ""} } // BookPicB esBook.BookPicB = book.BookPicB // BookPicW if book.BookPicW != nil { esBook.BookPicW = book.BookPicW } else { esBook.BookPicW = make(map[string]interface{}) } // 插入到ES log.Printf("[BatchAddBookToESHandler] 准备插入书籍: ISBN=%s, BookName=%+v", book.ISBN, esBook.BookName) addedBook, err := svc.AddBookToES(c.Request.Context(), esBook) if err != nil { item.Error = fmt.Sprintf("插入ES失败: %v", err) result.Results = append(result.Results, item) result.FailedCount++ continue } item.Success = true item.Document = addedBook.ISBN // 使用ISBN作为文档标识 result.Results = append(result.Results, item) result.SuccessCount++ } // 根据整体结果返回相应的状态码 statusCode := 200 if result.SuccessCount == 0 { statusCode = 500 // 全部失败 } else if result.FailedCount > 0 { statusCode = 207 // 部分成功 (Multi-Status) } c.JSON(statusCode, gin.H{ "code": statusCode, "message": "批量插入完成", "data": result, }) } // validateISBNFormat 验证ISBN格式 func validateISBNFormat(isbn string) bool { if isbn == "" { return false } // 移除连字符和空格后检查是否只包含数字 cleanISBN := strings.ReplaceAll(strings.ReplaceAll(isbn, "-", ""), " ", "") if len(cleanISBN) < 10 || len(cleanISBN) > 13 { return false } // 检查是否只包含数字 for _, r := range cleanISBN { if r < '0' || r > '9' { return false } } return true } // CheckBookExistsRequest 检查书籍是否存在的请求结构体 type CheckBookExistsRequest struct { ISBN string `json:"isbn" binding:"required"` // 要查询的ISBN } // CheckBookExistsResponse 检查书籍是否存在的响应结构体 type CheckBookExistsResponse struct { Exists bool `json:"exists"` // 是否存在 ISBN string `json:"isbn"` // 查询的ISBN BookID string `json:"book_id"` // 书籍ID(如果存在) BookName string `json:"book_name"` // 书名(如果存在) Message string `json:"message"` // 提示信息 } // CheckBookExistsByISBN 根据ISBN检查书籍是否存在 func (svc *ESSearchService) CheckBookExistsByISBN(isbn string) *CheckBookExistsResponse { isbn = strings.TrimSpace(isbn) log.Printf("[CheckBookExistsByISBN] 开始检查 | ISBN=%s", isbn) if isbn == "" { return &CheckBookExistsResponse{ Exists: false, ISBN: "", Message: "ISBN不能为空", } } // 调用现有的SearchBookByISBN方法 book, err := svc.SearchBookByISBN(isbn) if err != nil { log.Printf("[CheckBookExistsByISBN] 查询失败: %v", err) return &CheckBookExistsResponse{ Exists: false, ISBN: isbn, Message: fmt.Sprintf("查询失败: %v", err), } } if book == nil { log.Printf("[CheckBookExistsByISBN] 未找到书籍 | ISBN=%s", isbn) return &CheckBookExistsResponse{ Exists: false, ISBN: isbn, Message: "未找到该ISBN对应的书籍", } } log.Printf("[CheckBookExistsByISBN] 找到书籍 | ISBN=%s, BookName=%s", isbn, book.BookName.Value) return &CheckBookExistsResponse{ Exists: true, ISBN: isbn, BookID: fmt.Sprintf("%d", book.ID), BookName: book.BookName.Value, Message: "书籍存在", } } // CheckBookExistsByISBNHandler 根据ISBN检查书籍是否存在的HTTP处理器 func (svc *ESSearchService) CheckBookExistsByISBNHandler(c *gin.Context) { // 支持GET和POST两种方式 var req CheckBookExistsRequest var _ error // GET方式:从query参数获取 if c.Request.Method == "GET" { isbn := strings.TrimSpace(c.Query("isbn")) if isbn == "" { c.JSON(400, gin.H{ "code": 400, "error": "缺少ISBN参数", "message": "请在请求中提供isbn参数,例如: ?isbn=9787020002207", }) return } req.ISBN = isbn } else { // POST方式:从JSON body获取 if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{ "code": 400, "error": "请求参数格式错误", "message": err.Error(), }) return } } // 调用业务逻辑 result := svc.CheckBookExistsByISBN(req.ISBN) // 根据结果返回不同的状态码 statusCode := 200 if !result.Exists && result.Message == "查询失败" { statusCode = 500 // 查询出错 } else if !result.Exists { statusCode = 404 // 未找到 } c.JSON(statusCode, gin.H{ "code": statusCode, "success": result.Exists, "data": result, }) } // ConvertRedisBookToESBook 将 Redis 中的 BookInfo 转换为 ESBook func ConvertRedisBookToESBook(redisBook *request.BookInfo) *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: 0, IsIllegal: 0, IsReturn: 0, IsFilter: "000000", } }