daShangDao_centerBook/es/es_search.go
2026-03-07 10:45:37 +08:00

3781 lines
104 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/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-v2"
// 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"` // 过滤字段
}
// 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,
}
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: book.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,
}
}
// 用于 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"` // 过滤字段
}
// 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) ([]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,
}
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", "2")
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" {
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" {
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)
}
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
fmt.Printf("[DEBUG] page=%d pageSize=%d from=%d\n", page, pageSize, from)
// ========== 构建 ES 查询 ==========
query := map[string]interface{}{
"from": from,
"size": pageSize,
"query": map[string]interface{}{
"bool": map[string]interface{}{
"must": must,
},
},
"sort": []map[string]interface{}{
{"update_time": map[string]interface{}{"order": "desc"}},
},
}
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", "2")
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()
// 读取响应
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))]))
}
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 == "" {
c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
return
}
ctx := context.Background()
db4Client, err := redisClient.GetClientByName("db4")
if err == nil {
val, err := db4Client.Get(ctx, isbn).Result()
if err == nil && val != "" {
log.Printf("[SearchBooksHandler] 从 Redis db4 查询到数据: %s", isbn)
var esBook ESBook
if err := json.Unmarshal([]byte(val), &esBook); err == nil {
responseList := []ESBookResponse{esBook.ConvertToResponse()}
c.JSON(200, gin.H{
"count": 1,
"data": responseList,
})
return
} else {
log.Printf("[SearchBooksHandler] 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("db4")
fmt.Println(db4Client)
if err != nil {
log.Printf("[SearchBookByISBNHandler] 获取 Redis db4 客户端失败: %v", err)
} else {
val, err := db4Client.Get(ctx, isbn).Result()
if err == nil && val != "" {
log.Printf("[SearchBookByISBNHandler] 从 Redis db4 查询到数据: %s", isbn)
var esBook ESBook
if err := json.Unmarshal([]byte(val), &esBook); err == nil {
responseData := esBook.ConvertToResponse()
c.JSON(200, gin.H{
"data": responseData,
})
return
} else {
log.Printf("[SearchBookByISBNHandler] Redis 数据解析失败: %v", err)
}
} else {
log.Printf("[SearchBookByISBNHandler] Redis db4 中未找到 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",
"全1", "全2", "全3", "全4", "全5", "全6", "全7", "全八", "全9",
"共11", "共12", "共13", "共14", "共15", "共16", "共17", "共18", "共19",
"全二", "全三", "全四", "全五", "全六", "全七", "全八", "全九",
"2本合售", "3本合售", "4本合售", "5本合售", "6本合售", "7本合售", "8本合售", "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册", "全3卷", "全4卷", "全5卷", "全6卷", "全7卷", "全8卷", "全合拾肆卷", "合拾伍卷", "合拾陆卷", "合拾柒卷", "合拾捌卷", "合拾玖卷",
"共贰卷", "共叁卷", "共肆卷", "共伍卷", "共陆卷", "共柒卷", "共捌卷", "共玖卷", "共拾卷",
"共拾壹卷", "共拾贰卷", "共拾叁卷", "共拾肆卷", "共拾伍卷", "共拾陆卷", "共拾柒卷", "共拾捌卷", "共拾玖卷",
"全贰卷", "全叁卷", "全肆卷", "全伍卷", "全陆卷", "全柒卷", "全捌卷", "全玖卷", "全拾卷",
"全拾壹卷", "全拾贰卷", "全拾叁卷", "全拾肆卷", "全拾伍卷", "全拾陆卷", "全拾柒卷", "全拾捌卷", "全拾玖卷",
"合2辑", "合3辑", "合4辑", "合5辑", "合6辑", "合7辑", "合8辑", "合9辑",
"合10辑", "合11辑", "合12辑", "合13辑", "合14辑", "合15辑", "合16辑", "合17辑", "合18辑", "合19辑",
"共2辑", "共3辑", "共4辑", "共5辑", "共6辑", "卷", "全12卷", "全13卷", "全14卷", "册", "合四册", "合五册", "合六册", "合七册", "合八册", "合九册", "合十册",
"十六本", "合十七本", "合十八本", "合十九本",
"共二本", "共三本", "共", "一套八本", "一套九本", "一套十本", "一套十一本",
"1-1", "1-2", "1-3", "共十七", "共十八", "共十九",
}
// 检查书名是否包含任何一个套装关键字
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,
},
},
}
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)
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,
}
for field, value := range data {
if !allowedFields[field] {
continue
}
scriptParts = append(scriptParts,
fmt.Sprintf("ctx._source.%s = params.%s;", field, field))
params[field] = value
}
if len(scriptParts) == 0 {
return 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() {
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,
})
}