daShangDao_centerBook/es/es_search.go
2026-03-28 19:15:27 +08:00

4269 lines
124 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}},
},
},
},
// 情况2keyword字段存在但为空字符串
{
"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}},
},
},
},
// 情况2keyword字段存在但为空字符串
{
"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",
}
}