4565 lines
133 KiB
Go
4565 lines
133 KiB
Go
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-v2"
|
||
const ESIndexV3 = "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"` // 扩展字段,用于兼容未来新增字段
|
||
}
|
||
|
||
// ESBookResponseByPSI 用于返回给Java客户端的格式,ID为简单的int64
|
||
type ESBookResponseByPSI struct {
|
||
ID int64 `json:"id"`
|
||
BookName string `json:"book_name"`
|
||
BookPic map[string]interface{} `json:"book_pic"`
|
||
BookPicS map[string]interface{} `json:"book_pic_s"`
|
||
BookPicB string `json:"book_pic_b"`
|
||
BookPicW map[string]interface{} `json:"book_pic_w"`
|
||
BookDefPic map[string]interface{} `json:"book_def_pic"` // 自制官图,临时用
|
||
ISBN string `json:"isbn"`
|
||
Author string `json:"author"`
|
||
Category string `json:"category"`
|
||
Publisher string `json:"publisher"`
|
||
PublicationTime string `json:"publication_time"`
|
||
BindingLayout string `json:"binding_layout"`
|
||
FixPrice float64 `json:"fix_price"`
|
||
Content string `json:"content"`
|
||
IsSuit int `json:"is_suit"`
|
||
DaySale7 int `json:"day_sale_7"`
|
||
DaySale15 int `json:"day_sale_15"`
|
||
DaySale30 int `json:"day_sale_30"`
|
||
DaySale60 int `json:"day_sale_60"`
|
||
DaySale90 int `json:"day_sale_90"`
|
||
DaySale180 int `json:"day_sale_180"`
|
||
DaySale365 int `json:"day_sale_365"`
|
||
ThisYearSale int `json:"this_year_sale"`
|
||
LastYearSale int `json:"last_year_sale"`
|
||
TotalSale int `json:"total_sale"`
|
||
BuyCounts int64 `json:"buy_counts"`
|
||
SellCounts int64 `json:"sell_counts"`
|
||
BookPicObj map[string]interface{} `json:"book_pic_obj"`
|
||
BookPicObjS map[string]interface{} `json:"book_pic_obj_s"`
|
||
UpdateTime NumberOrString `json:"update_time"`
|
||
IsIllegal int `json:"is_illegal"` // 是否非法 示例 000000
|
||
IsReturn int `json:"is_return"` // 是否为驳回 示例 0 否 1 是
|
||
IsFilter string `json:"is_filter"` // 过滤字段
|
||
PageCount NumberOrString `json:"page_count"` // 页数
|
||
WordCount NumberOrString `json:"word_count"` // 字数
|
||
BookFormat NumberOrString `json:"book_format"` // 多少开
|
||
CatId request.CatIdObject `json:"cat_id"` // 类目
|
||
Other map[string]interface{} `json:"other"` // 扩展字段,用于兼容未来新增字段
|
||
}
|
||
|
||
// FlexibleString 处理可能是字符串或数组的字段
|
||
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")
|
||
} else {
|
||
if timeVal, err := strconv.ParseInt(publicationTime, 10, 64); err == nil {
|
||
publicationTime = strconv.FormatInt(timeVal-5364000000, 10)
|
||
}
|
||
}
|
||
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,
|
||
}
|
||
}
|
||
|
||
// ConvertToResponseByPsi 将ESBook转换为ESBookResponse,处理ID字段转换
|
||
func (book *ESBook) ConvertToResponseByPsi() ESBookResponseByPSI {
|
||
bookPicMap := map[string]interface{}{
|
||
"localPath": book.BookPic.LocalPath,
|
||
"pddPath": book.BookPic.PddPath,
|
||
}
|
||
bookPicSMap := map[string]interface{}{
|
||
"localPath": book.BookPicS.LocalPath,
|
||
"pddResponse": book.BookPicS.PddResponse,
|
||
}
|
||
bookDefPic := map[string]interface{}{
|
||
"localPath": book.BookDefPic.LocalPath,
|
||
"pddPath": book.BookDefPic.PddPath,
|
||
}
|
||
publicationTime := book.PublicationTime
|
||
if publicationTime == "" || publicationTime == "0" || publicationTime == "0000-00-00" {
|
||
publicationTime = time.Unix(0, 0).Format("2006-01")
|
||
} else {
|
||
if timeVal, err := strconv.ParseInt(publicationTime, 10, 64); err == nil {
|
||
publicationTime = strconv.FormatInt(timeVal-5364000000, 10)
|
||
}
|
||
}
|
||
return ESBookResponseByPSI{
|
||
ID: book.ID,
|
||
BookName: book.BookName.Value,
|
||
BookPic: bookPicMap,
|
||
BookPicS: bookPicSMap,
|
||
BookPicB: book.BookPicB,
|
||
BookPicW: book.BookPicW,
|
||
BookDefPic: bookDefPic,
|
||
ISBN: book.ISBN,
|
||
Author: book.Author,
|
||
Category: book.Category,
|
||
Publisher: book.Publisher,
|
||
PublicationTime: publicationTime,
|
||
BindingLayout: book.BindingLayout,
|
||
FixPrice: float64(book.FixPrice),
|
||
Content: book.Content,
|
||
IsSuit: book.IsSuit,
|
||
DaySale7: book.DaySale7,
|
||
DaySale15: book.DaySale15,
|
||
DaySale30: book.DaySale30,
|
||
DaySale60: book.DaySale60,
|
||
DaySale90: book.DaySale90,
|
||
DaySale180: book.DaySale180,
|
||
DaySale365: book.DaySale365,
|
||
ThisYearSale: book.ThisYearSale,
|
||
LastYearSale: book.LastYearSale,
|
||
TotalSale: book.TotalSale,
|
||
BuyCounts: book.BuyCounts,
|
||
SellCounts: book.SellCounts,
|
||
BookPicObj: book.BookPicObj,
|
||
BookPicObjS: book.BookPicObjS,
|
||
UpdateTime: book.UpdateTime,
|
||
IsIllegal: book.IsIllegal,
|
||
IsReturn: book.IsReturn,
|
||
IsFilter: book.IsFilter,
|
||
PageCount: book.PageCount,
|
||
WordCount: book.WordCount,
|
||
BookFormat: book.BookFormat,
|
||
CatId: book.CatId,
|
||
Other: book.Other,
|
||
}
|
||
}
|
||
|
||
// 用于 book_pic
|
||
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"`
|
||
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,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
|
||
SyncRedisByISBN func(isbn string, act string) error
|
||
}
|
||
|
||
type NumberOrString string
|
||
|
||
func (n *NumberOrString) UnmarshalJSON(data []byte) error {
|
||
s := strings.TrimSpace(string(data))
|
||
|
||
// 如果是数字(不以引号开头)
|
||
if len(s) > 0 && s[0] != '"' {
|
||
*n = NumberOrString(s)
|
||
return nil
|
||
}
|
||
|
||
// 普通字符串
|
||
var str string
|
||
if err := json.Unmarshal(data, &str); err != nil {
|
||
return err
|
||
}
|
||
*n = NumberOrString(str)
|
||
return nil
|
||
}
|
||
|
||
type Float64OrString float64
|
||
|
||
func (f *Float64OrString) UnmarshalJSON(b []byte) error {
|
||
// 去掉空值
|
||
if string(b) == "null" || len(b) == 0 {
|
||
*f = 0
|
||
return nil
|
||
}
|
||
|
||
// 尝试解析为 float
|
||
var num float64
|
||
if err := json.Unmarshal(b, &num); err == nil {
|
||
*f = Float64OrString(num)
|
||
return nil
|
||
}
|
||
|
||
// 尝试解析为 string
|
||
var str string
|
||
if err := json.Unmarshal(b, &str); err == nil {
|
||
if str == "" {
|
||
*f = 0
|
||
return nil
|
||
}
|
||
parsed, err := strconv.ParseFloat(str, 64)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
*f = Float64OrString(parsed)
|
||
return nil
|
||
}
|
||
|
||
return fmt.Errorf("无法解析 fix_price: %s", string(b))
|
||
}
|
||
|
||
func NewESSearchService(es *ESClient) *ESSearchService {
|
||
return &ESSearchService{ES: es}
|
||
}
|
||
|
||
// validateBookPicObj 验证 BookPic 对象格式
|
||
func validateBookPicObj(bookPic map[string]interface{}, fieldName string) error {
|
||
// 允许book_pic为nil,直接返回成功
|
||
if bookPic == nil {
|
||
return nil
|
||
}
|
||
|
||
// 检查字段是否存在,不存在则使用默认空字符串
|
||
localPath, hasLocalPath := bookPic["localPath"]
|
||
if !hasLocalPath {
|
||
localPath = ""
|
||
bookPic["localPath"] = ""
|
||
}
|
||
|
||
pddPath, hasPddPath := bookPic["pddPath"]
|
||
if !hasPddPath {
|
||
pddPath = ""
|
||
bookPic["pddPath"] = ""
|
||
}
|
||
|
||
// 检查字段类型,允许空字符串,但必须是字符串类型
|
||
if _, ok := localPath.(string); !ok {
|
||
return fmt.Errorf("%s localPath 必须为字符串类型", fieldName)
|
||
}
|
||
|
||
if _, ok := pddPath.(string); !ok {
|
||
return fmt.Errorf("%s pddPath 必须为字符串类型", fieldName)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// validateBookPicSObj 验证 BookPicS 对象格式
|
||
func validateBookPicSObj(bookPicS map[string]interface{}, fieldName string) error {
|
||
// 允许book_pic_s为nil,直接返回成功
|
||
if bookPicS == nil {
|
||
return nil
|
||
}
|
||
|
||
// 检查字段是否存在,不存在则使用默认空字符串
|
||
localPath, hasLocalPath := bookPicS["localPath"]
|
||
if !hasLocalPath {
|
||
localPath = ""
|
||
bookPicS["localPath"] = ""
|
||
}
|
||
|
||
pddResponse, hasPddResponse := bookPicS["pddResponse"]
|
||
if !hasPddResponse {
|
||
pddResponse = ""
|
||
bookPicS["pddResponse"] = ""
|
||
}
|
||
|
||
// 检查字段类型,允许空字符串,但必须是字符串类型
|
||
if _, ok := localPath.(string); !ok {
|
||
return fmt.Errorf("%s localPath 必须为字符串类型", fieldName)
|
||
}
|
||
|
||
if _, ok := pddResponse.(string); !ok {
|
||
return fmt.Errorf("%s pddResponse 必须为字符串类型", fieldName)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// validateBookPicWObj 验证 BookPicW 对象格式
|
||
func validateBookPicWObj(bookPicW map[string]interface{}, fieldName string) error {
|
||
if bookPicW == nil {
|
||
return fmt.Errorf("%s 不能为 nil", fieldName)
|
||
}
|
||
|
||
// BookPicW 是任意对象,只需要验证不是空map
|
||
if len(bookPicW) == 0 {
|
||
return fmt.Errorf("%s 不能为空对象", fieldName)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// validateBookPicObjS 验证 BookPicObjS 对象格式(类似于 BookPicS)
|
||
func validateBookPicObjS(bookPicObjS map[string]interface{}, fieldName string) error {
|
||
return validateBookPicSObj(bookPicObjS, fieldName)
|
||
}
|
||
|
||
type esHitsWrapper struct {
|
||
Hits struct {
|
||
Total struct {
|
||
Value int `json:"value"`
|
||
} `json:"total"`
|
||
Hits []struct {
|
||
Index string `json:"_index"`
|
||
ID string `json:"_id"`
|
||
Source ESBook `json:"_source"`
|
||
} `json:"hits"`
|
||
} `json:"hits"`
|
||
}
|
||
|
||
// SearchBooks 搜索图书
|
||
func (svc *ESSearchService) SearchBooks(keyword string, endpoint ...string) ([]ESBook, error) {
|
||
|
||
keyword = strings.TrimSpace(keyword)
|
||
if keyword == "" {
|
||
return []ESBook{}, nil
|
||
}
|
||
|
||
query := map[string]interface{}{
|
||
"_source": []string{
|
||
"id", "book_name", "book_pic", "book_pic_s",
|
||
"isbn", "author", "category", "publisher",
|
||
"publication_time", "binding_layout", "fix_price",
|
||
"content", "update_time",
|
||
},
|
||
"query": map[string]interface{}{
|
||
"multi_match": map[string]interface{}{
|
||
"query": keyword,
|
||
"type": "best_fields",
|
||
"fields": []string{"book_name^5", "isbn^10", "author^3"},
|
||
"fuzziness": "AUTO",
|
||
},
|
||
},
|
||
}
|
||
|
||
body, err := json.Marshal(query)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("序列化请求失败: %v", err)
|
||
}
|
||
|
||
req := esapi.SearchRequest{
|
||
Index: []string{ESIndex},
|
||
Body: bytes.NewReader(body),
|
||
TrackTotalHits: true,
|
||
}
|
||
|
||
// 如果有传入 endpoint,使用监控
|
||
var res *esapi.Response
|
||
var duration time.Duration
|
||
|
||
if len(endpoint) > 0 && endpoint[0] != "" {
|
||
monitoredES := monitor.NewMonitoredESClient(svc.ES.Client, endpoint[0])
|
||
res, duration, err = monitoredES.Search(context.Background(), &req)
|
||
log.Printf("[SearchBooks] ES 查询耗时:%dms", duration.Milliseconds())
|
||
} else {
|
||
// 原有逻辑,不带监控
|
||
res, err = req.Do(context.Background(), svc.ES.Client.Transport)
|
||
}
|
||
if err != nil {
|
||
return nil, fmt.Errorf("执行 ES 查询失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return nil, fmt.Errorf("ES 返回错误: %s", res.String())
|
||
}
|
||
|
||
var parsed esHitsWrapper
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
return nil, fmt.Errorf("解析 ES 响应失败: %v", err)
|
||
}
|
||
|
||
list := make([]ESBook, 0, len(parsed.Hits.Hits))
|
||
for _, hit := range parsed.Hits.Hits {
|
||
list = append(list, hit.Source)
|
||
}
|
||
|
||
return list, nil
|
||
}
|
||
|
||
// SearchBookByISBN 通过 ISBN 查询图书
|
||
func (svc *ESSearchService) SearchBookByISBN(isbn string) (*ESBook, error) {
|
||
isbn = strings.TrimSpace(isbn)
|
||
log.Printf("[SearchBookByISBN] 开始查询 | ISBN=%s", isbn)
|
||
|
||
if isbn == "" {
|
||
log.Printf("[SearchBookByISBN] ISBN 为空,取消查询")
|
||
return nil, fmt.Errorf("ISBN 不能为空")
|
||
}
|
||
|
||
query := map[string]interface{}{
|
||
"query": map[string]interface{}{
|
||
"term": map[string]interface{}{
|
||
"isbn": isbn, // 精准匹配
|
||
},
|
||
},
|
||
"_source": true,
|
||
}
|
||
|
||
body, err := json.Marshal(query)
|
||
if err != nil {
|
||
log.Printf("[SearchBookByISBN] 构建查询 JSON 失败: %v", err)
|
||
return nil, fmt.Errorf("构建查询 JSON 失败: %v", err)
|
||
}
|
||
|
||
// 执行查询
|
||
res, err := svc.ES.Client.Search(
|
||
svc.ES.Client.Search.WithIndex(ESIndex),
|
||
svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
|
||
svc.ES.Client.Search.WithTrackTotalHits(true),
|
||
)
|
||
if err != nil {
|
||
log.Printf("[SearchBookByISBN] ES 查询失败: %v", err)
|
||
return nil, fmt.Errorf("ES 查询失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
//log.Printf("[SearchBookByISBN] ES 响应: %s", res.String())
|
||
if res.IsError() {
|
||
log.Printf("[SearchBookByISBN] ES 返回错误: %s", res.String())
|
||
return nil, fmt.Errorf("ES 返回错误: %s", res.String())
|
||
}
|
||
|
||
var parsed esHitsWrapper
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
log.Printf("[SearchBookByISBN] 解析 ES 返回失败: %v", err)
|
||
return nil, fmt.Errorf("解析 ES 返回失败: %v", err)
|
||
}
|
||
|
||
if len(parsed.Hits.Hits) == 0 {
|
||
log.Printf("[SearchBookByISBN] 未找到 ISBN=%s 对应文档", isbn)
|
||
return nil, nil
|
||
}
|
||
|
||
// 取第一条结果
|
||
book := parsed.Hits.Hits[0].Source
|
||
//log.Printf("[SearchBookByISBN] 查询到文档: %+v", book)
|
||
|
||
return &book, nil
|
||
}
|
||
|
||
func (svc *ESSearchService) SearchBookByBookName(bookName string) ([]ESBook, error) {
|
||
|
||
bookName = strings.TrimSpace(bookName)
|
||
if bookName == "" {
|
||
return nil, fmt.Errorf("bookName 不能为空")
|
||
}
|
||
|
||
query := map[string]interface{}{
|
||
"_source": true,
|
||
"query": map[string]interface{}{
|
||
"match": map[string]interface{}{
|
||
"book_name": map[string]interface{}{
|
||
"query": bookName,
|
||
"operator": "and",
|
||
"fuzziness": "AUTO",
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
body, _ := json.Marshal(query)
|
||
|
||
res, err := svc.ES.Client.Search(
|
||
svc.ES.Client.Search.WithIndex(ESIndex),
|
||
svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
|
||
svc.ES.Client.Search.WithTrackTotalHits(true),
|
||
)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ES 查询失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return nil, fmt.Errorf("ES 返回错误: %s", res.String())
|
||
}
|
||
|
||
var parsed esHitsWrapper
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
return nil, fmt.Errorf("解析 ES 返回失败: %v", err)
|
||
}
|
||
|
||
list := make([]ESBook, 0, len(parsed.Hits.Hits))
|
||
for _, hit := range parsed.Hits.Hits {
|
||
list = append(list, hit.Source)
|
||
}
|
||
|
||
return list, nil
|
||
}
|
||
|
||
// SearchBooksAllFields 单字段匹配
|
||
func (svc *ESSearchService) SearchBooksAllFields(keyword string) ([]ESBook, error) {
|
||
|
||
keyword = strings.TrimSpace(keyword)
|
||
if keyword == "" {
|
||
return []ESBook{}, nil
|
||
}
|
||
|
||
query := map[string]interface{}{
|
||
"_source": true,
|
||
"query": map[string]interface{}{
|
||
"multi_match": map[string]interface{}{
|
||
"query": keyword,
|
||
"type": "best_fields", // 使用最佳字段评分模式
|
||
"fields": []string{
|
||
"id^1",
|
||
"book_name^5",
|
||
"book_pic^1",
|
||
"book_pic_s^1",
|
||
"isbn^10",
|
||
"author^4",
|
||
"category^2",
|
||
"publisher^3",
|
||
"publication_time^1",
|
||
"binding_layout^1",
|
||
"fix_price^1",
|
||
"content^1",
|
||
"update_time^1",
|
||
},
|
||
"fuzziness": "AUTO", // 自动纠错/模糊匹配
|
||
},
|
||
},
|
||
}
|
||
|
||
body, _ := json.Marshal(query)
|
||
|
||
res, err := svc.ES.Client.Search(
|
||
svc.ES.Client.Search.WithIndex(ESIndex),
|
||
svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
|
||
svc.ES.Client.Search.WithTrackTotalHits(true),
|
||
)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("执行 ES 查询失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return nil, fmt.Errorf("ES 返回错误: %s", res.String())
|
||
}
|
||
|
||
var parsed esHitsWrapper
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
return nil, fmt.Errorf("解析 ES 响应失败: %v", err)
|
||
}
|
||
|
||
list := make([]ESBook, 0, len(parsed.Hits.Hits))
|
||
for _, hit := range parsed.Hits.Hits {
|
||
list = append(list, hit.Source)
|
||
}
|
||
|
||
return list, nil
|
||
}
|
||
|
||
// SearchBooksByConditions 多字段AND搜索
|
||
func (svc *ESSearchService) SearchBooksByConditions(conds map[string]string, page, pageSize int) ([]ESBook, int, error) {
|
||
mustQueries := make([]map[string]interface{}, 0)
|
||
|
||
for field, value := range conds {
|
||
value = strings.TrimSpace(value)
|
||
if value == "" {
|
||
continue
|
||
}
|
||
|
||
// 特殊处理 day_sale_30 做范围查询
|
||
if field == "day_sale_30" {
|
||
// 这里假设前端传入 "1",表示查询 >1
|
||
if v, err := strconv.Atoi(value); err == nil {
|
||
mustQueries = append(mustQueries, map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
"day_sale_30": map[string]interface{}{
|
||
"gt": v,
|
||
},
|
||
},
|
||
})
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 其他字段用 match 查询
|
||
mustQueries = append(mustQueries, map[string]interface{}{
|
||
"match": map[string]interface{}{
|
||
field: map[string]interface{}{
|
||
"query": value,
|
||
"operator": "and",
|
||
"fuzziness": "AUTO",
|
||
},
|
||
},
|
||
})
|
||
}
|
||
|
||
if len(mustQueries) == 0 {
|
||
return []ESBook{}, 0, nil
|
||
}
|
||
|
||
from := (page - 1) * pageSize
|
||
|
||
query := map[string]interface{}{
|
||
"_source": true,
|
||
"from": from,
|
||
"size": pageSize,
|
||
"query": map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must": mustQueries,
|
||
},
|
||
},
|
||
}
|
||
|
||
body, _ := json.Marshal(query)
|
||
|
||
res, err := svc.ES.Client.Search(
|
||
svc.ES.Client.Search.WithIndex(ESIndex),
|
||
svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
|
||
svc.ES.Client.Search.WithTrackTotalHits(true),
|
||
)
|
||
if err != nil {
|
||
return nil, 0, fmt.Errorf("ES 查询失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return nil, 0, fmt.Errorf("ES 错误: %s", res.String())
|
||
}
|
||
|
||
var parsed esHitsWrapper
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
return nil, 0, fmt.Errorf("解析失败: %v", err)
|
||
}
|
||
|
||
list := make([]ESBook, 0, len(parsed.Hits.Hits))
|
||
for _, hit := range parsed.Hits.Hits {
|
||
list = append(list, hit.Source)
|
||
}
|
||
|
||
return list, parsed.Hits.Total.Value, nil
|
||
}
|
||
|
||
// CountByIDRange 统计 ID 范围内的数量
|
||
func (svc *ESSearchService) CountByIDRange(minID, maxID int) (int, error) {
|
||
|
||
query := map[string]interface{}{
|
||
"query": map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
"id": map[string]interface{}{
|
||
"gte": minID,
|
||
"lte": maxID,
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
body, _ := json.Marshal(query)
|
||
|
||
res, err := svc.ES.Client.Count(
|
||
svc.ES.Client.Count.WithIndex(ESIndex),
|
||
svc.ES.Client.Count.WithBody(bytes.NewReader(body)),
|
||
)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return 0, fmt.Errorf("ES 错误: %s", res.String())
|
||
}
|
||
|
||
var resp struct {
|
||
Count int `json:"count"`
|
||
}
|
||
|
||
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
return resp.Count, nil
|
||
}
|
||
|
||
// UpdateBookPicByISBN 根据 ISBN 更新 ES 文档的图片字段
|
||
// 功能:更新 `book_pic` 与 `book_pic_s` 字段(任意一个或两个)
|
||
// 入参:
|
||
// - isbn: 目标文档的 ISBN(精确匹配)
|
||
// - bookPic: 新的大图地址(可为空,空则不更新该字段)
|
||
// - bookPicS: 新的小图地址(可为空,空则不更新该字段)
|
||
// 返回:成功更新的文档数量
|
||
func (svc *ESSearchService) UpdateBookPicByISBN(isbn, bookPic, bookPicS string) (int, error) {
|
||
isbn = strings.TrimSpace(isbn)
|
||
if isbn == "" {
|
||
return 0, fmt.Errorf("ISBN 不能为空")
|
||
}
|
||
// 构建脚本:仅对提供的字段进行更新
|
||
scriptParts := make([]string, 0, 2)
|
||
if strings.TrimSpace(bookPic) != "" {
|
||
scriptParts = append(scriptParts, "ctx._source.book_pic = params.book_pic;")
|
||
}
|
||
if strings.TrimSpace(bookPicS) != "" {
|
||
scriptParts = append(scriptParts, "ctx._source.book_pic_s = params.book_pic_s;")
|
||
}
|
||
if len(scriptParts) == 0 {
|
||
return 0, fmt.Errorf("至少提供 book_pic 或 book_pic_s 其中之一")
|
||
}
|
||
|
||
source := strings.Join(scriptParts, " ")
|
||
|
||
body := map[string]interface{}{
|
||
"script": map[string]interface{}{
|
||
"source": source,
|
||
"lang": "painless",
|
||
"params": map[string]interface{}{
|
||
"book_pic": bookPic,
|
||
"book_pic_s": bookPicS,
|
||
},
|
||
},
|
||
"query": map[string]interface{}{
|
||
"term": map[string]interface{}{
|
||
"isbn": isbn,
|
||
},
|
||
},
|
||
}
|
||
|
||
payload, _ := json.Marshal(body)
|
||
|
||
res, err := svc.ES.Client.UpdateByQuery(
|
||
[]string{ESIndex},
|
||
svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)),
|
||
svc.ES.Client.UpdateByQuery.WithRefresh(true),
|
||
svc.ES.Client.UpdateByQuery.WithConflicts("proceed"),
|
||
)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("ES UpdateByQuery 执行失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return 0, fmt.Errorf("ES 返回错误: %s", res.String())
|
||
}
|
||
|
||
// 解析更新数量
|
||
var parsed struct {
|
||
Total int `json:"total"`
|
||
Updated int `json:"updated"`
|
||
VersionConflicts int `json:"version_conflicts"`
|
||
}
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
return 0, fmt.Errorf("解析 ES UpdateByQuery 响应失败: %v", err)
|
||
}
|
||
|
||
return parsed.Updated, nil
|
||
}
|
||
|
||
// UpdateSellCountsByISBN 根据 ISBN 更新 ES 文档的 sell_counts 字段
|
||
// 功能:将指定 ISBN 的文档字段 `sell_counts` 更新为给定值
|
||
// 入参:
|
||
// - isbn: 精确匹配目标文档的 ISBN
|
||
// - sellCounts: 在售数量(非负整数)
|
||
// 返回:成功更新的文档数量
|
||
func (svc *ESSearchService) UpdateSellCountsByISBN(isbn string, sellCounts int) (int, error) {
|
||
isbn = strings.TrimSpace(isbn)
|
||
if isbn == "" {
|
||
return 0, fmt.Errorf("ISBN 不能为空")
|
||
}
|
||
if sellCounts < 0 {
|
||
return 0, fmt.Errorf("sell_counts 不可为负数")
|
||
}
|
||
|
||
body := map[string]interface{}{
|
||
"script": map[string]interface{}{
|
||
"source": "ctx._source.sell_counts = params.sell_counts;",
|
||
"lang": "painless",
|
||
"params": map[string]interface{}{
|
||
"sell_counts": sellCounts,
|
||
},
|
||
},
|
||
"query": map[string]interface{}{
|
||
"term": map[string]interface{}{
|
||
"isbn": isbn,
|
||
},
|
||
},
|
||
}
|
||
|
||
payload, _ := json.Marshal(body)
|
||
|
||
res, err := svc.ES.Client.UpdateByQuery(
|
||
[]string{ESIndex},
|
||
svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)),
|
||
svc.ES.Client.UpdateByQuery.WithRefresh(true),
|
||
svc.ES.Client.UpdateByQuery.WithConflicts("proceed"),
|
||
)
|
||
if err != nil {
|
||
return 0, fmt.Errorf("ES UpdateByQuery 执行失败: %v", err)
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
if res.IsError() {
|
||
return 0, fmt.Errorf("ES 返回错误: %s", res.String())
|
||
}
|
||
|
||
var parsed struct {
|
||
Total int `json:"total"`
|
||
Updated int `json:"updated"`
|
||
VersionConflicts int `json:"version_conflicts"`
|
||
}
|
||
if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
|
||
return 0, fmt.Errorf("解析 ES UpdateByQuery 响应失败: %v", err)
|
||
}
|
||
|
||
return parsed.Updated, nil
|
||
}
|
||
|
||
// UpdateSellCountsByISBNHandler 根据请求的 ISBN 调用外部接口获取在售数量并更新 ES
|
||
// 功能:
|
||
// 1) 读取 `isbn` 参数;
|
||
// 2) 调用 tail.GetOnSaleCount 获取在售数量;
|
||
// 3) 调用 UpdateSellCountsByISBN 更新 ES;
|
||
// 4) 返回更新结果与在售数量。
|
||
func (svc *ESSearchService) UpdateSellCountsByISBNHandler(c *gin.Context) {
|
||
// 异步执行:立即返回成功,后台更新 ES
|
||
isbn := strings.TrimSpace(c.Query("isbn"))
|
||
if isbn == "" {
|
||
c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}})
|
||
return
|
||
}
|
||
|
||
// 获取在售数量(容错 data:{})
|
||
onSaleCount, err := tail.GetOnSaleCount(isbn)
|
||
log.Printf("[Async] 获取在售数量 | ISBN=%s | count=%d", isbn, onSaleCount)
|
||
if err != nil {
|
||
c.JSON(500, gin.H{"error": "获取在售数量失败", "details": err.Error()})
|
||
return
|
||
}
|
||
if onSaleCount == 0 {
|
||
c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}})
|
||
return
|
||
}
|
||
|
||
// 后台异步更新
|
||
go func(isbn string, count int) {
|
||
updated, err := svc.UpdateSellCountsByISBN(isbn, count)
|
||
if err != nil {
|
||
log.Printf("[Async] 更新 ES 失败 | ISBN=%s | err=%v", isbn, err)
|
||
return
|
||
}
|
||
log.Printf("[Async] 更新 ES 成功 | ISBN=%s | sell_counts=%d | updated=%d", isbn, count, updated)
|
||
}(isbn, onSaleCount)
|
||
|
||
// 立即返回成功,携带异步提示
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": gin.H{"isbn": isbn, "on_sale_count": onSaleCount, "async": true},
|
||
})
|
||
}
|
||
|
||
// UpdateSellCountsDirectHandler 直接按入参异步更新 ES 的在售数量
|
||
// 功能:
|
||
// - 从请求中读取 `isbn` 与 `onSaleCount`;
|
||
// - 校验参数(ISBN 非空、onSaleCount >= 0 且为整数);
|
||
// - 后台异步调用 ES 更新 `sell_counts` 字段;
|
||
// - 立即返回标准成功结构,包含 async 提示;当 onSaleCount=0 时直接返回 success 不更新。
|
||
func (svc *ESSearchService) UpdateSellCountsDirectHandler(c *gin.Context) {
|
||
isbn := strings.TrimSpace(c.Query("isbn"))
|
||
countStr := strings.TrimSpace(c.Query("onSaleCount"))
|
||
|
||
if isbn == "" {
|
||
c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
|
||
return
|
||
}
|
||
if countStr == "" {
|
||
c.JSON(400, gin.H{"error": "缺少 onSaleCount 参数"})
|
||
return
|
||
}
|
||
|
||
count, err := strconv.Atoi(countStr)
|
||
if err != nil {
|
||
c.JSON(400, gin.H{"error": "onSaleCount 必须为整数", "details": err.Error()})
|
||
return
|
||
}
|
||
if count < 0 {
|
||
c.JSON(400, gin.H{"error": "onSaleCount 不可为负数"})
|
||
return
|
||
}
|
||
// 如果为 0,直接返回 success,不做更新
|
||
if count == 0 {
|
||
c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}})
|
||
return
|
||
}
|
||
|
||
// 后台异步更新 ES 的 sell_counts 字段
|
||
go func(isbn string, c int) {
|
||
updated, err := svc.UpdateSellCountsByISBN(isbn, c)
|
||
if err != nil {
|
||
log.Printf("[Async] 直接更新 ES 失败 | ISBN=%s | err=%v", isbn, err)
|
||
return
|
||
}
|
||
log.Printf("[Async] 直接更新 ES 成功 | ISBN=%s | sell_counts=%d | updated=%d", isbn, c, updated)
|
||
}(isbn, count)
|
||
|
||
// 立即返回成功,携带异步提示
|
||
c.JSON(200, gin.H{
|
||
"code": 200,
|
||
"message": "success",
|
||
"data": gin.H{"isbn": isbn, "sell_counts": count, "async": true},
|
||
})
|
||
}
|
||
|
||
// SearchBookBaseInfoES 根据条件查询 ES 图书信息
|
||
func (svc *ESSearchService) SearchBookBaseInfoES(c *gin.Context) ([]ESBook, int, error) {
|
||
|
||
q := c.Request.URL.Query()
|
||
|
||
must := make([]map[string]interface{}, 0)
|
||
|
||
// ES 字段映射
|
||
snakeMap := map[string]string{
|
||
"bookName": "book_name",
|
||
"bookPic": "book_pic",
|
||
"publication_times": "publication_time",
|
||
"isSuit": "is_suit",
|
||
}
|
||
|
||
// ===== saleSelect 对应字段映射 =====
|
||
saleSelect := c.DefaultQuery("saleSelect", "")
|
||
saleField := map[string]string{
|
||
"7": "day_sale_7",
|
||
"15": "day_sale_15",
|
||
"30": "day_sale_30",
|
||
"60": "day_sale_60",
|
||
"90": "day_sale_90",
|
||
"180": "day_sale_180",
|
||
"365": "day_sale_365",
|
||
"0": "this_year_sale",
|
||
"1": "last_year_sale",
|
||
}[saleSelect]
|
||
|
||
fmt.Printf("[DEBUG] saleSelect=%s saleField=%s\n", saleSelect, saleField)
|
||
|
||
// saleField >= 1 默认条件
|
||
if saleField != "" {
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
saleField: map[string]interface{}{"gte": 1},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
}
|
||
|
||
// per_page → pageSize
|
||
if perPage := c.Query("per_page"); perPage != "" {
|
||
q.Set("pageSize", perPage)
|
||
}
|
||
|
||
// ========== 遍历参数 ==========
|
||
|
||
for key, vals := range q {
|
||
val := strings.TrimSpace(vals[0])
|
||
if val == "" {
|
||
continue
|
||
}
|
||
|
||
if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" || key == "shopType" {
|
||
continue
|
||
}
|
||
|
||
// 忽略无用字段
|
||
if key == "is_pricing" ||
|
||
key == "vio_book" ||
|
||
key == "book_set" ||
|
||
key == "onenum_mbooks" ||
|
||
key == "ill_publisher" ||
|
||
key == "ill_author" {
|
||
continue
|
||
}
|
||
|
||
originalKey := key
|
||
fmt.Printf("[DEBUG] Processing key=%s val=%s\n", key, val)
|
||
|
||
// 字段映射
|
||
if nk, ok := snakeMap[key]; ok {
|
||
key = nk
|
||
}
|
||
|
||
// ===== is_suit =====
|
||
if key == "is_suit" {
|
||
fmt.Printf("[DEBUG] is_suit val=%q\n", val)
|
||
if num, err := strconv.Atoi(val); err == nil {
|
||
cond := map[string]interface{}{
|
||
"term": map[string]interface{}{"is_suit": num},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
} else {
|
||
fmt.Printf("[ERROR] is_suit Atoi error: %v\n", err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
// ===== is_return =====
|
||
if key == "is_return" {
|
||
fmt.Printf("[DEBUG] is_return val=%q\n", val)
|
||
if num, err := strconv.Atoi(val); err == nil {
|
||
cond := map[string]interface{}{
|
||
"term": map[string]interface{}{"is_return": num},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
} else {
|
||
fmt.Printf("[ERROR] is_return Atoi error: %v\n", err)
|
||
}
|
||
continue
|
||
}
|
||
|
||
// ===== is_filter =====
|
||
// ===== is_filter(按 shopType 位匹配)=====
|
||
// ===== is_filter(按 shopType 位匹配)=====
|
||
if key == "is_filter" {
|
||
fmt.Printf("[DEBUG] is_filter val=%q\n", val)
|
||
|
||
// 只处理 1(=1) 和 2(=0)
|
||
if val != "1" && val != "2" {
|
||
continue
|
||
}
|
||
|
||
shopType := c.DefaultQuery("shopType", "")
|
||
var pattern string
|
||
|
||
// val=1 -> 位=1
|
||
// val=2 -> 位=0
|
||
targetBit := "1"
|
||
if val == "2" {
|
||
targetBit = "0"
|
||
}
|
||
|
||
switch shopType {
|
||
case "0":
|
||
pattern = targetBit + "*"
|
||
case "1":
|
||
pattern = "?" + targetBit + "*"
|
||
case "2":
|
||
pattern = "??" + targetBit + "*"
|
||
case "3":
|
||
pattern = "???" + targetBit + "*"
|
||
default:
|
||
continue
|
||
}
|
||
|
||
cond := map[string]interface{}{
|
||
"wildcard": map[string]interface{}{
|
||
"is_filter": pattern,
|
||
},
|
||
}
|
||
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
// ===== categoryType =====
|
||
if originalKey == "categoryType" {
|
||
var cond map[string]interface{}
|
||
if val == "1" {
|
||
cond = map[string]interface{}{
|
||
"prefix": map[string]interface{}{"isbn": "9787"},
|
||
}
|
||
} else {
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must_not": []map[string]interface{}{
|
||
{"prefix": map[string]interface{}{"isbn": "9787"}},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
// ===== category =====
|
||
if key == "category" {
|
||
var cond map[string]interface{}
|
||
if val == "排除大学教材" {
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must_not": []map[string]interface{}{
|
||
{"match_phrase": map[string]interface{}{
|
||
"category": "图书/教材教辅考试/大学教材",
|
||
}},
|
||
},
|
||
},
|
||
}
|
||
} else {
|
||
cond = map[string]interface{}{
|
||
"match_phrase": map[string]interface{}{"category": val},
|
||
}
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// =====================================================
|
||
// ========== ★★★ book_pic + picType 联动逻辑 ★★★ ==========
|
||
// =====================================================
|
||
if key == "book_pic" {
|
||
|
||
picType := c.DefaultQuery("picType", "1") // 默认官图
|
||
|
||
var targetField string
|
||
if picType == "1" {
|
||
targetField = "book_pic.pddPath"
|
||
} else {
|
||
targetField = "book_pic_s.pddResponse"
|
||
}
|
||
|
||
var cond map[string]interface{}
|
||
if val == "1" {
|
||
// 有图:字段必须存在且非空
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must": []map[string]interface{}{
|
||
{"exists": map[string]interface{}{"field": targetField}},
|
||
{"wildcard": map[string]interface{}{
|
||
targetField: "*",
|
||
}},
|
||
},
|
||
},
|
||
}
|
||
} else {
|
||
// 无图:使用keyword字段查询字段不存在或为空字符串
|
||
var keywordField string
|
||
if picType == "1" {
|
||
keywordField = "book_pic.pddPath.keyword"
|
||
} else {
|
||
keywordField = "book_pic_s.pddResponse.keyword"
|
||
}
|
||
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"should": []map[string]interface{}{
|
||
// 情况1:字段完全不存在
|
||
{
|
||
"bool": map[string]interface{}{
|
||
"must_not": []map[string]interface{}{
|
||
{"exists": map[string]interface{}{"field": keywordField}},
|
||
},
|
||
},
|
||
},
|
||
// 情况2:keyword字段存在但为空字符串
|
||
{
|
||
"term": map[string]interface{}{
|
||
keywordField: "",
|
||
},
|
||
},
|
||
},
|
||
"minimum_should_match": 1,
|
||
},
|
||
}
|
||
}
|
||
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// ===== buy_counts → saleField =====
|
||
if key == "buy_counts" {
|
||
fmt.Printf("[DEBUG] buy_counts uses saleField=%s\n", saleField)
|
||
if saleField == "" {
|
||
continue
|
||
}
|
||
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
saleField: map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
if num, err := strconv.Atoi(val); err == nil {
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
saleField: map[string]interface{}{
|
||
"gte": num,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== totalSale_range =====
|
||
if key == "totalSale_range" {
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
"total_sale": map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== 数值范围 =====
|
||
if key == "sell_counts" ||
|
||
strings.HasPrefix(key, "day_sale_") ||
|
||
key == "this_year_sale" || key == "last_year_sale" ||
|
||
key == "publication_time" {
|
||
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
key: map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== 精确匹配 =====
|
||
if key == "isbn" || key == "id" || key == "publisher" {
|
||
cond := map[string]interface{}{
|
||
"term": map[string]interface{}{key: val},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// ===== 模糊匹配 =====
|
||
if key == "book_name" || key == "author" {
|
||
cond := map[string]interface{}{
|
||
"match": map[string]interface{}{
|
||
key: map[string]interface{}{
|
||
"query": val,
|
||
"operator": "and",
|
||
"fuzziness": "AUTO",
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// ===== 默认前缀匹配 =====
|
||
cond := map[string]interface{}{
|
||
"prefix": map[string]interface{}{key: val},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
}
|
||
|
||
// ========== 分页 ==========
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10")))
|
||
from := (page - 1) * pageSize
|
||
// ========== pageSize大于500则根据id查询 =========
|
||
fmt.Printf("[DEBUG] page=%d pageSize=%d from=%d\n", page, pageSize, from)
|
||
var sort []map[string]interface{}
|
||
if pageSize >= 500 {
|
||
sort = []map[string]interface{}{
|
||
{"id": map[string]interface{}{"order": "asc"}},
|
||
}
|
||
} else {
|
||
sort = []map[string]interface{}{
|
||
{"update_time": map[string]interface{}{"order": "desc"}},
|
||
}
|
||
}
|
||
// ========== 构建 ES 查询 ==========
|
||
query := map[string]interface{}{
|
||
"from": from,
|
||
"size": pageSize,
|
||
"query": map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must": must,
|
||
},
|
||
},
|
||
|
||
"sort": sort,
|
||
}
|
||
|
||
body, _ := json.MarshalIndent(query, "", " ")
|
||
fmt.Printf("[DEBUG] ES Query Body:\n%s\n", string(body))
|
||
|
||
// ========== 执行 ES 查询 ==========
|
||
res, err := svc.ES.Client.Search(
|
||
svc.ES.Client.Search.WithIndex(ESIndex),
|
||
svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
|
||
svc.ES.Client.Search.WithTrackTotalHits(true),
|
||
)
|
||
|
||
if err != nil {
|
||
fmt.Printf("[ERROR] ES.Client.Search error: %v\n", err)
|
||
return nil, 0, err
|
||
}
|
||
defer res.Body.Close()
|
||
|
||
// 读取响应
|
||
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
|
||
}
|
||
|
||
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" || key == "kongfz_include" {
|
||
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
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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(按 shopType 位匹配)=====
|
||
if key == "is_filter" {
|
||
fmt.Printf("[DEBUG] is_filter val=%q\n", val)
|
||
|
||
// 只处理 1(=1) 和 2(=0)
|
||
if val != "1" && val != "2" {
|
||
continue
|
||
}
|
||
|
||
shopType := c.DefaultQuery("shopType", "")
|
||
var pattern string
|
||
|
||
// val=1 -> 位=1
|
||
// val=2 -> 位=0
|
||
targetBit := "1"
|
||
if val == "2" {
|
||
targetBit = "0"
|
||
}
|
||
|
||
switch shopType {
|
||
case "0":
|
||
pattern = targetBit + "*"
|
||
case "1":
|
||
pattern = "?" + targetBit + "*"
|
||
case "2":
|
||
pattern = "??" + targetBit + "*"
|
||
case "3":
|
||
pattern = "???" + targetBit + "*"
|
||
default:
|
||
continue
|
||
}
|
||
|
||
cond := map[string]interface{}{
|
||
"wildcard": map[string]interface{}{
|
||
"is_filter": pattern,
|
||
},
|
||
}
|
||
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// ===== categoryType =====
|
||
if originalKey == "categoryType" {
|
||
var cond map[string]interface{}
|
||
if val == "1" {
|
||
cond = map[string]interface{}{
|
||
"prefix": map[string]interface{}{"isbn": "9787"},
|
||
}
|
||
} else {
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must_not": []map[string]interface{}{
|
||
{"prefix": map[string]interface{}{"isbn": "9787"}},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// ===== category =====
|
||
if key == "category" {
|
||
var cond map[string]interface{}
|
||
if val == "排除大学教材" {
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must_not": []map[string]interface{}{
|
||
{"match_phrase": map[string]interface{}{
|
||
"category": "图书/教材教辅考试/大学教材",
|
||
}},
|
||
},
|
||
},
|
||
}
|
||
} else {
|
||
cond = map[string]interface{}{
|
||
"match_phrase": map[string]interface{}{"category": val},
|
||
}
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// =====================================================
|
||
// ========== ★★★ book_pic + picType 联动逻辑 ★★★ ==========
|
||
// =====================================================
|
||
if key == "book_pic" {
|
||
|
||
picType := c.DefaultQuery("picType", "1") // 默认官图
|
||
|
||
var targetField string
|
||
if picType == "1" {
|
||
targetField = "book_pic.pddPath"
|
||
} else {
|
||
targetField = "book_pic_s.pddResponse"
|
||
}
|
||
|
||
var cond map[string]interface{}
|
||
if val == "1" {
|
||
// 有图:字段必须存在且非空
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must": []map[string]interface{}{
|
||
{"exists": map[string]interface{}{"field": targetField}},
|
||
{"wildcard": map[string]interface{}{
|
||
targetField: "*",
|
||
}},
|
||
},
|
||
},
|
||
}
|
||
} else {
|
||
// 无图:使用keyword字段查询字段不存在或为空字符串
|
||
var keywordField string
|
||
if picType == "1" {
|
||
keywordField = "book_pic.pddPath.keyword"
|
||
} else {
|
||
keywordField = "book_pic_s.pddResponse.keyword"
|
||
}
|
||
|
||
cond = map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"should": []map[string]interface{}{
|
||
// 情况1:字段完全不存在
|
||
{
|
||
"bool": map[string]interface{}{
|
||
"must_not": []map[string]interface{}{
|
||
{"exists": map[string]interface{}{"field": keywordField}},
|
||
},
|
||
},
|
||
},
|
||
// 情况2:keyword字段存在但为空字符串
|
||
{
|
||
"term": map[string]interface{}{
|
||
keywordField: "",
|
||
},
|
||
},
|
||
},
|
||
"minimum_should_match": 1,
|
||
},
|
||
}
|
||
}
|
||
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
// ===== buy_counts → saleField =====
|
||
if key == "buy_counts" {
|
||
fmt.Printf("[DEBUG] buy_counts uses saleField=%s\n", saleField)
|
||
if saleField == "" {
|
||
continue
|
||
}
|
||
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
saleField: map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
|
||
if num, err := strconv.Atoi(val); err == nil {
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
saleField: map[string]interface{}{
|
||
"gte": num,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== totalSale_range =====
|
||
if key == "totalSale_range" {
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
"total_sale": map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v\n", cond)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== page_count 范围查询 =====
|
||
if key == "page_count" {
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
"page_count": map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v (page_count: %d-%d)\n", cond, minVal, maxVal)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== word_count 范围查询 =====
|
||
if key == "word_count" {
|
||
parts := strings.Split(val, ",")
|
||
if len(parts) == 2 {
|
||
minVal, _ := strconv.Atoi(parts[0])
|
||
maxVal, _ := strconv.Atoi(parts[1])
|
||
|
||
cond := map[string]interface{}{
|
||
"range": map[string]interface{}{
|
||
"word_count": map[string]interface{}{
|
||
"gte": minVal,
|
||
"lte": maxVal,
|
||
},
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v (word_count: %d-%d)\n", cond, minVal, maxVal)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// ===== kongfz_categories(孔夫子分类)=====
|
||
if key == "kongfz_categories" {
|
||
kongfzInclude := c.DefaultQuery("kongfz_include", "1")
|
||
|
||
// 分割多个分类ID
|
||
categories := strings.Split(val, ",")
|
||
if len(categories) > 0 {
|
||
// 过滤空值
|
||
var validCategories []string
|
||
for _, catID := range categories {
|
||
catID = strings.TrimSpace(catID)
|
||
if catID != "" {
|
||
validCategories = append(validCategories, catID)
|
||
}
|
||
}
|
||
|
||
if len(validCategories) > 0 {
|
||
if kongfzInclude == "2" {
|
||
// 否:查询不包含这些分类的数据(must_not)
|
||
var mustNotQueries []map[string]interface{}
|
||
for _, catID := range validCategories {
|
||
mustNotQueries = append(mustNotQueries, map[string]interface{}{
|
||
"prefix": map[string]interface{}{
|
||
"cat_id.kong_fu_zi_cat_id": catID,
|
||
},
|
||
})
|
||
}
|
||
|
||
if len(mustNotQueries) > 0 {
|
||
cond := map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"must_not": mustNotQueries,
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v (kongfz exclude: %d categories)\n", cond, len(mustNotQueries))
|
||
}
|
||
} else {
|
||
// 是:查询包含这些分类的数据(should + minimum_should_match=1)
|
||
var shouldQueries []map[string]interface{}
|
||
for _, catID := range validCategories {
|
||
shouldQueries = append(shouldQueries, map[string]interface{}{
|
||
"prefix": map[string]interface{}{
|
||
"cat_id.kong_fu_zi_cat_id": catID,
|
||
},
|
||
})
|
||
}
|
||
|
||
if len(shouldQueries) > 0 {
|
||
cond := map[string]interface{}{
|
||
"bool": map[string]interface{}{
|
||
"should": shouldQueries,
|
||
"minimum_should_match": 1,
|
||
},
|
||
}
|
||
must = append(must, cond)
|
||
fmt.Printf("[DEBUG] must += %v (kongfz include: %d categories)\n", cond, len(shouldQueries))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
// ===== 数值范围 =====
|
||
if key == "sell_counts" ||
|
||
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))
|
||
|
||
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("db11")
|
||
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("db11")
|
||
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) SearchByISBNByXyCallBack(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()
|
||
|
||
// 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 {
|
||
c.JSON(200, gin.H{
|
||
"data": nil,
|
||
})
|
||
return
|
||
}
|
||
|
||
responseData := result.ConvertToResponse()
|
||
c.JSON(200, gin.H{
|
||
"data": responseData,
|
||
})
|
||
}
|
||
|
||
func (svc *ESSearchService) SearchBookByISBNHandlerToPsi(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("db10")
|
||
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.BookInfoByPsi
|
||
if err := json.Unmarshal([]byte(val), &redisBook); err == nil {
|
||
esBook := ConvertRedisBookToESBookByPsi(&redisBook)
|
||
if esBook != nil {
|
||
responseData := esBook.ConvertToResponseByPsi()
|
||
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 {
|
||
c.JSON(200, gin.H{
|
||
"data": nil,
|
||
})
|
||
return
|
||
}
|
||
|
||
responseData := result.ConvertToResponseByPsi()
|
||
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,
|
||
},
|
||
},
|
||
}
|
||
|
||
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,
|
||
"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
|
||
}
|
||
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())
|
||
}
|
||
|
||
// 同步 Redis
|
||
if svc.SyncRedisByISBN != nil {
|
||
_ = svc.SyncRedisByISBN(req.ISBN, "update")
|
||
}
|
||
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",
|
||
}
|
||
}
|
||
|
||
// ConvertRedisBookToESBookByPsi 将 Redis 中的 BookInfo 转换为 ESBook
|
||
func ConvertRedisBookToESBookByPsi(redisBook *request.BookInfoByPsi) *ESBook {
|
||
if redisBook == nil || redisBook.Isbn == "" {
|
||
return nil
|
||
}
|
||
|
||
// 构建图片对象
|
||
carouselUrls := []string{}
|
||
if len(redisBook.ImageObject.CarouselUrlArray) > 0 {
|
||
carouselUrls = redisBook.ImageObject.CarouselUrlArray
|
||
}
|
||
|
||
liveShootingUrls := []string{}
|
||
if len(redisBook.ImageObject.DetailUrlObject.LiveShootingUrl) > 0 {
|
||
liveShootingUrls = redisBook.ImageObject.DetailUrlObject.LiveShootingUrl
|
||
}
|
||
|
||
// 提取第一张轮播图作为 book_pic
|
||
bookPicPath := ""
|
||
if len(carouselUrls) > 0 {
|
||
bookPicPath = carouselUrls[0]
|
||
}
|
||
|
||
// 提取第一张实拍图作为 book_pic_s
|
||
bookPicSResponse := ""
|
||
if len(liveShootingUrls) > 0 {
|
||
bookPicSResponse = liveShootingUrls[0]
|
||
}
|
||
|
||
return &ESBook{
|
||
ISBN: redisBook.Isbn,
|
||
BookName: FlexibleString{Value: redisBook.BookName},
|
||
Author: redisBook.Author,
|
||
Publisher: redisBook.Publishing,
|
||
PublicationTime: redisBook.PublicationDate,
|
||
BindingLayout: redisBook.Binding,
|
||
PageCount: NumberOrString(strconv.FormatInt(redisBook.PagesCount, 10)),
|
||
WordCount: NumberOrString(strconv.FormatInt(redisBook.WordsCount, 10)),
|
||
BookFormat: NumberOrString(strconv.FormatInt(redisBook.Format, 10)),
|
||
FixPrice: Float64OrString(redisBook.Price),
|
||
BookPic: BookPicObj{
|
||
LocalPath: "",
|
||
PddPath: bookPicPath,
|
||
},
|
||
BookPicS: BookPicSObj{
|
||
LocalPath: "",
|
||
PddResponse: bookPicSResponse,
|
||
},
|
||
BookDefPic: BookDefPicObj{
|
||
LocalPath: "",
|
||
PddPath: redisBook.ImageObject.DefaultImageUrl,
|
||
},
|
||
BookPicB: redisBook.ImageObject.WhiteBackgroundUrl,
|
||
CatId: redisBook.CatIdObject,
|
||
// 销量等字段默认为 0
|
||
DaySale7: 0,
|
||
DaySale15: 0,
|
||
DaySale30: 0,
|
||
DaySale60: 0,
|
||
DaySale90: 0,
|
||
DaySale180: 0,
|
||
DaySale365: 0,
|
||
ThisYearSale: 0,
|
||
LastYearSale: 0,
|
||
TotalSale: 0,
|
||
BuyCounts: 0,
|
||
SellCounts: 0,
|
||
IsSuit: redisBook.IsSuit,
|
||
IsIllegal: 0,
|
||
IsReturn: 0,
|
||
IsFilter: "000000",
|
||
}
|
||
}
|