diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/AugmentWebviewStateStore.xml b/.idea/AugmentWebviewStateStore.xml
new file mode 100644
index 0000000..c649f55
--- /dev/null
+++ b/.idea/AugmentWebviewStateStore.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/centerBook.iml b/.idea/centerBook.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/centerBook.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..cf247ce
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..457fe9f
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/controller/pddCatId.go b/controller/pddCatId.go
new file mode 100644
index 0000000..86741bf
--- /dev/null
+++ b/controller/pddCatId.go
@@ -0,0 +1,58 @@
+package controller
+
+import (
+ "centerBook/util/esClient"
+ "centerBook/util/pdd"
+ "centerBook/util/redisClient"
+ "context"
+ "log"
+)
+
+// 推测pddCatId类目
+func getPddCatId() {
+
+ ctx := context.Background()
+ // 为两个数据库创建命名客户端
+ redisClient.AddClient("db4", "36.212.20.113", "j8nZ4jra2E", 4)
+ redisClient.AddClient("db14", "36.212.20.113", "j8nZ4jra2E", 14)
+
+ // 从 db4 获取数据示例
+ db4Client, err := redisClient.GetClientByName("db4")
+ if err == nil {
+ // 示例:获取键为 "key1" 的值
+ val, err := db4Client.Get(ctx, "1995373681100910593").Result()
+ if err == nil {
+ log.Println(val)
+ }
+ }
+
+ bookName := ""
+
+ instance, err := pdd.GetPddInstance()
+ if err != nil {
+ return
+ }
+ instance.PddGoodsOuterCatMappingGet("", "15543", "书籍/杂志/报纸", "书籍 "+bookName)
+
+ // 从 db14 获取数据示例
+ db14Client, err := redisClient.GetClientByName("db14")
+ if err == nil {
+ // 示例:获取键为 "key2" 的值
+ val, err := db14Client.Get(ctx, "key2").Result()
+ if err == nil {
+ // 处理获取到的值
+ }
+ }
+
+}
+
+// connectES 连接到 Elasticsearch
+func connectES(addresses []string, username, password string) (*esClient.ESClient, error) {
+ return esClient.NewESClient(addresses, username, password)
+}
+
+// connectRedis 连接到 Redis
+func connectRedis(addr, password string, db int) {
+ redisClient.InitRedis(addr, password, db)
+ redisClient.GetClient()
+}
diff --git a/erp/erp_api.go b/erp/erp_api.go
new file mode 100644
index 0000000..3f0579c
--- /dev/null
+++ b/erp/erp_api.go
@@ -0,0 +1,89 @@
+package erp
+
+import (
+ "crypto/rand"
+ "database/sql"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "math/big"
+ "net/http"
+ "time"
+)
+
+type InsertFilterSetRequest struct {
+ AddTxt string `json:"add_txt" binding:"required"`
+}
+type Response struct {
+ Code int `json:"code"`
+ Msg string `json:"msg"`
+ Data interface{} `json:"data"`
+}
+
+// HandleInsertFilterSet 返回一个用于插入 ERP 过滤设置的处理器。
+// 用途:解析请求体并将过滤设置写入 zhishu 数据库,返回插入结果。
+// 参数:
+// - db: zhishu 数据库连接(必需)
+// 行为:
+// 1) 校验参数;2) 执行插入;3) 返回插入的记录 ID。
+func HandleInsertFilterSet(db *sql.DB) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var req InsertFilterSetRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, Response{Code: 500, Msg: "请求参数错误: " + err.Error(), Data: nil})
+ return
+ }
+
+ if db == nil {
+ c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "ERP数据库未初始化", Data: nil})
+ return
+ }
+ id, err := insertFilterSet(db, req.AddTxt)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, Response{Code: 500, Msg: "插入失败: " + err.Error(), Data: nil})
+ return
+ }
+ c.JSON(http.StatusOK, Response{Code: 200, Msg: "插入成功", Data: map[string]interface{}{"id": id}})
+ }
+}
+
+// insertFilterSet 将过滤设置写入 ERP (zhishu) 数据库。
+// 参数 db 为 zhishu 数据库连接,bookName 为过滤项内容。
+// 返回值:新记录的主键 ID;若失败返回错误。
+func insertFilterSet(db *sql.DB, bookName string) (int64, error) {
+ if db == nil {
+ return 0, fmt.Errorf("Zhishu数据库未初始化")
+ }
+ id := generateRandom19DigitID() // 随机 19 位数字
+ stmt := "INSERT INTO t_filter_set (id, filter_type, limitation_type, add_way, add_txt, sort, create_time) VALUES (?, ?, ?, ?, ?, ?, ?)"
+ _, err := db.Exec(stmt, id, "1", "1", "0", bookName, "1", time.Now())
+ if err != nil {
+ return 0, err
+ }
+ return id, nil
+}
+
+// 生成 19 位随机数字 ID
+// 生成 19 位随机数字 ID(不会溢出)
+func generateRandom19DigitID() int64 {
+ min, _ := new(big.Int).SetString("1000000000000000000", 10) // 10^18
+ max, _ := new(big.Int).SetString("9999999999999999999", 10) // 10^19 - 1
+ diff := new(big.Int).Sub(max, min)
+
+ n, err := rand.Int(rand.Reader, diff)
+ if err != nil {
+ // 如果随机生成失败,则使用时间戳作为回退
+ return time.Now().UnixNano()
+ }
+
+ return new(big.Int).Add(n, min).Int64()
+}
+
+// RegisterERPRoutes 注册 ERP 相关的路由到 Gin 引擎。
+// 用途:集中管理 ERP 接口的路由挂载,避免在 main.go 中分散注册。
+// 参数:
+// - r: Gin 引擎实例
+// - db: zhishu 数据库连接
+// 当前注册:插入过滤设置接口,可在此处继续扩展其他 ERP 路由。
+func RegisterERPRoutes(r *gin.Engine, db *sql.DB) {
+ r.POST("/api/erp/insertFilterSet", HandleInsertFilterSet(db))
+}
diff --git a/es/DLL测试说明.md b/es/DLL测试说明.md
new file mode 100644
index 0000000..93c2ac8
--- /dev/null
+++ b/es/DLL测试说明.md
@@ -0,0 +1,307 @@
+# es.dll 测试说明
+
+## 测试文件说明
+
+项目中有三种测试方式:
+
+### 1. 单元测试文件 (`es_dll_test.go`)
+使用 Go 标准的测试框架,包含完整的单元测试。
+
+### 2. 命令行测试工具 (`test_dll.go`)
+交互式命令行工具,可以手动测试各种功能。
+
+### 3. 演示测试
+自动化的完整演示流程。
+
+---
+
+## 方法一:运行单元测试
+
+### 运行所有测试
+```bash
+cd es
+go test -v
+```
+
+### 运行单个测试
+```bash
+cd es
+go test -v -run TestListAllIndices
+go test -v -run TestCreateDocument
+go test -v -run TestCompleteWorkflow
+```
+
+### 查看测试覆盖率
+```bash
+go test -cover
+```
+
+---
+
+## 方法二:使用命令行测试工具
+
+### 基本语法
+```bash
+# 在项目根目录运行
+go run test_es_dll.go <命令> [参数...]
+```
+
+### 查看帮助
+```bash
+go run test_es_dll.go
+```
+
+### 常用命令示例
+
+#### 1. 查询所有索引
+```bash
+go run test_dll.go list
+```
+
+#### 2. 获取所有索引详细信息
+```bash
+go run test_dll.go info
+```
+
+#### 3. 创建索引
+```bash
+go run test_dll.go create my_index '{"properties":{"title":{"type":"text"},"author":{"type":"keyword"}}}'
+```
+
+#### 4. 查看索引详情
+```bash
+go run test_dll.go detail my_index
+```
+
+#### 5. 创建文档
+```bash
+go run test_dll.go add my_index doc1 '{"title":"测试文档","author":"张三","content":"这是测试内容"}'
+```
+
+#### 6. 获取文档
+```bash
+go run test_dll.go get my_index doc1
+```
+
+#### 7. 更新文档
+```bash
+go run test_dll.go update my_index doc1 '{"doc":{"title":"更新后的标题"}}'
+```
+
+#### 8. 搜索文档
+```bash
+# 搜索所有文档
+go run test_dll.go search my_index '{"query":{"match_all":{}}}'
+
+# 搜索特定内容
+go run test_dll.go search my_index '{"query":{"match":{"title":"测试"}}}'
+
+# 带分页的搜索
+go run test_dll.go search my_index '{"query":{"match_all":{}},"from":0,"size":10}'
+```
+
+#### 9. 获取文档数量
+```bash
+go run test_dll.go count my_index
+```
+
+#### 10. 删除文档
+```bash
+go run test_dll.go del_doc my_index doc1
+```
+
+#### 11. 删除索引
+```bash
+go run test_dll.go delete my_index
+```
+
+#### 12. 运行完整演示
+```bash
+go run test_dll.go test
+```
+
+---
+
+## 方法三:在代码中使用
+
+### 示例代码
+
+```go
+package main
+
+import (
+ "centerBook/es"
+ "encoding/json"
+ "fmt"
+ "log"
+)
+
+func main() {
+ // 1. 初始化DLL
+ dll, err := es.InitEsDLL()
+ if err != nil {
+ log.Fatalf("初始化失败: %v", err)
+ }
+ defer dll.Close()
+
+ // 2. 查询所有索引
+ result, err := dll.ListAllIndices()
+ if err != nil {
+ log.Fatalf("查询失败: %v", err)
+ }
+
+ // 3. 解析JSON结果
+ var data map[string]interface{}
+ json.Unmarshal([]byte(result), &data)
+ fmt.Printf("所有索引: %v\n", data)
+
+ // 4. 创建索引
+ mapping := `{
+ "properties": {
+ "title": {"type": "text"},
+ "author": {"type": "keyword"}
+ }
+ }`
+ result, _ = dll.CreateIndex("my_index", mapping)
+ fmt.Printf("创建索引结果: %s\n", result)
+
+ // 5. 添加文档
+ doc := `{"title": "Go语言编程", "author": "张三"}`
+ result, _ = dll.CreateDocument("my_index", "doc1", doc)
+ fmt.Printf("添加文档结果: %s\n", result)
+
+ // 6. 搜索文档
+ query := `{"query": {"match": {"title": "Go"}}}`
+ result, _ = dll.SearchDocuments("my_index", query)
+ fmt.Printf("搜索结果: %s\n", result)
+
+ // 7. 清理
+ dll.DeleteDocument("my_index", "doc1")
+ dll.DeleteIndex("my_index")
+}
+```
+
+---
+
+## DLL 接口说明
+
+所有方法返回的格式都是 JSON 字符串:
+
+### 成功响应格式
+```json
+{
+ "success": true,
+ "message": "",
+ "data": {...}
+}
+```
+
+### 错误响应格式
+```json
+{
+ "success": false,
+ "message": "错误信息"
+}
+```
+
+---
+
+## 注意事项
+
+1. **DLL 文件位置**
+ - 确保 `dll/es.dll` 文件存在于项目根目录
+ - 如果提示找不到 DLL,检查路径是否正确
+
+2. **JSON 参数格式**
+ - 在命令行中使用时,JSON 参数需要用单引号包裹
+ - Windows CMD 可能需要转义引号,建议使用 PowerShell
+
+3. **错误处理**
+ - 所有方法都返回 `(string, error)`
+ - 使用时检查 error 是否为 nil
+
+4. **资源释放**
+ - 使用 `defer dll.Close()` 确保 DLL 资源被释放
+ - C 字符串指针会自动释放
+
+---
+
+## 快速测试流程
+
+### 完整测试流程(推荐新手)
+
+```bash
+# 1. 运行自动演示(最简单)
+cd es
+go run test_dll.go test
+
+# 2. 手动测试各个功能
+# 查看现有索引
+go run test_dll.go list
+
+# 创建测试索引
+go run test_dll.go create test '{"properties":{"title":{"type":"text"}}}'
+
+# 添加文档
+go run test_dll.go add test doc1 '{"title":"Hello World"}'
+
+# 查询文档
+go run test_dll.go get test doc1
+
+# 搜索
+go run test_dll.go search test '{"query":{"match_all":{}}}'
+
+# 删除文档
+go run test_dll.go del_doc test doc1
+
+# 删除索引
+go run test_dll.go delete test
+```
+
+### 运行单元测试(推荐验证功能)
+
+```bash
+cd es
+
+# 运行所有测试
+go test -v
+
+# 运行完整工作流测试
+go test -v -run TestCompleteWorkflow
+```
+
+---
+
+## 常见问题
+
+### Q: 提示找不到 DLL
+**A:** 检查 `dll/es.dll` 文件是否存在,路径是否正确
+
+### Q: JSON 格式错误
+**A:** 确保 JSON 字符串格式正确,使用在线工具验证
+
+### Q: 命令行中 JSON 参数太复杂
+**A:** 将 JSON 保存到文件,然后读取:
+```bash
+# Linux/Mac
+go run test_dll.go search test "$(cat query.json)"
+
+# Windows PowerShell
+go run test_dll.go search test (Get-Content query.json -Raw)
+```
+
+---
+
+## 测试检查清单
+
+- [ ] DLL 能正常加载
+- [ ] 能查询所有索引
+- [ ] 能创建索引
+- [ ] 能创建文档
+- [ ] 能获取文档
+- [ ] 能更新文档
+- [ ] 能搜索文档
+- [ ] 能删除文档
+- [ ] 能删除索引
+- [ ] C 字符串正确释放
+- [ ] 资源正确释放
diff --git a/es/es_client.go b/es/es_client.go
new file mode 100644
index 0000000..cfc7a88
--- /dev/null
+++ b/es/es_client.go
@@ -0,0 +1,51 @@
+package es
+
+import (
+ "crypto/tls"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/elastic/go-elasticsearch/v8"
+)
+
+// ESClient 封装 Elasticsearch 客户端
+type ESClient struct {
+ Client *elasticsearch.Client
+}
+
+// NewESClient 初始化客户端
+func NewESClient(addresses []string, username, password string) (*ESClient, error) {
+
+ cfg := elasticsearch.Config{
+ Addresses: addresses,
+ Username: username,
+ Password: password,
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ MaxIdleConnsPerHost: 100,
+ ResponseHeaderTimeout: 60 * time.Second,
+ },
+ CompressRequestBody: true,
+ }
+
+ client, err := elasticsearch.NewClient(cfg)
+ if err != nil {
+ return nil, fmt.Errorf("创建 ES 客户端失败: %v", err)
+ }
+
+ res, err := client.Info()
+ 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())
+ }
+
+ fmt.Println("✅ Elasticsearch 连接成功")
+ return &ESClient{Client: client}, nil
+}
diff --git a/es/es_dll.go b/es/es_dll.go
new file mode 100644
index 0000000..4103720
--- /dev/null
+++ b/es/es_dll.go
@@ -0,0 +1,265 @@
+package es
+
+//
+//import (
+// "fmt"
+// "os"
+// "path/filepath"
+// "syscall"
+// "unicode/utf16"
+// "unsafe"
+//)
+//
+//// EsDLL Elasticsearch工具DLL结构
+//type EsDLL struct {
+// dll *syscall.DLL
+// listAllIndices *syscall.Proc // 查询所有索引
+// getIndicesInfo *syscall.Proc // 获取所有索引的详细信息
+// getIndexDetail *syscall.Proc // 获取单个索引的详细信息
+// createIndex *syscall.Proc // 创建索引
+// deleteIndex *syscall.Proc // 删除索引
+// getDocumentCount *syscall.Proc // 获取索引文档数量
+// createDocument *syscall.Proc // 创建文档
+// getDocument *syscall.Proc // 根据ID获取文档
+// updateDocument *syscall.Proc // 更新文档
+// deleteDocument *syscall.Proc // 删除文档
+// searchDocuments *syscall.Proc // 搜索文档
+// freeCString *syscall.Proc // 释放C字符串
+//}
+//
+//// InitEsDLL 初始化esDLL
+//func InitEsDLL() (*EsDLL, error) {
+// // 尝试多个可能的DLL路径
+// dllPaths := []string{
+// filepath.Join("dll", "es.dll"),
+// filepath.Join("es_dll", "es.dll"),
+// "es.dll",
+// }
+//
+// var dllPath string
+// for _, path := range dllPaths {
+// if _, err := os.Stat(path); err == nil {
+// dllPath = path
+// break
+// }
+// }
+//
+// if dllPath == "" {
+// return nil, fmt.Errorf("es DLL 不存在,已尝试路径: %v", dllPaths)
+// }
+//
+// if dll, err := syscall.LoadDLL(dllPath); err != nil {
+// return nil, fmt.Errorf("加载es DLL 失败: %s", err)
+// } else {
+// return &EsDLL{
+// dll: dll,
+// listAllIndices: dll.MustFindProc("ListAllIndices"),
+// getIndicesInfo: dll.MustFindProc("GetIndicesInfo"),
+// getIndexDetail: dll.MustFindProc("GetIndexDetail"),
+// createIndex: dll.MustFindProc("CreateIndex"),
+// deleteIndex: dll.MustFindProc("DeleteIndex"),
+// getDocumentCount: dll.MustFindProc("GetDocumentCount"),
+// createDocument: dll.MustFindProc("CreateDocument"),
+// getDocument: dll.MustFindProc("GetDocument"),
+// updateDocument: dll.MustFindProc("UpdateDocument"),
+// deleteDocument: dll.MustFindProc("DeleteDocument"),
+// searchDocuments: dll.MustFindProc("SearchDocuments"),
+// freeCString: dll.MustFindProc("FreeCString"),
+// }, nil
+// }
+//}
+//
+//// cStr 获取C字符串
+//func (m *EsDLL) cStr(p uintptr) string {
+// if p == 0 {
+// return ""
+// }
+// b := []byte{}
+// for i := uintptr(0); ; i++ {
+// c := *(*byte)(unsafe.Pointer(p + i))
+// if c == 0 {
+// break
+// }
+// b = append(b, c)
+// }
+// s := string(b)
+// if m.freeCString != nil {
+// m.freeCString.Call(p)
+// }
+// return s
+//}
+//
+//// stringToUTF16Ptr 将Go字符串转换为UTF16指针(Windows)
+//func stringToUTF16Ptr(s string) uintptr {
+// if s == "" {
+// return 0
+// }
+// // 添加空终止符
+// ws := utf16.Encode([]rune(s + "\x00"))
+// return uintptr(unsafe.Pointer(&ws[0]))
+//}
+//
+//// stringToUTF8Ptr 将Go字符串转换为UTF8指针
+//func stringToUTF8Ptr(s string) uintptr {
+// if s == "" {
+// return 0
+// }
+// bytes := []byte(s)
+// // 添加空终止符
+// bytes = append(bytes, 0)
+// return uintptr(unsafe.Pointer(&bytes[0]))
+//}
+//
+//// strPtr 将Go字符串转换为C字符串指针
+//func (m *EsDLL) strPtr(s string) uintptr {
+// return stringToUTF8Ptr(s)
+//}
+//
+//// 1. 查询所有索引 - ListAllIndices
+//func (m *EsDLL) ListAllIndices() (string, error) {
+// proc, err := m.dll.FindProc("ListAllIndices")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 ListAllIndices: %v", err)
+// }
+// resultPtr, _, _ := proc.Call()
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 2. 获取所有索引的详细信息 - GetIndicesInfo
+//func (m *EsDLL) GetIndicesInfo() (string, error) {
+// proc, err := m.dll.FindProc("GetIndicesInfo")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 GetIndicesInfo: %v", err)
+// }
+// resultPtr, _, _ := proc.Call()
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 3. 获取单个索引的详细信息 - GetIndexDetail
+//func (m *EsDLL) GetIndexDetail(indexName string) (string, error) {
+// proc, err := m.dll.FindProc("GetIndexDetail")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 GetIndexDetail: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(stringToUTF8Ptr(indexName))
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 4. 创建索引 - CreateIndex
+//func (m *EsDLL) CreateIndex(indexName, mapping string) (string, error) {
+// proc, err := m.dll.FindProc("CreateIndex")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 CreateIndex: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(
+// stringToUTF8Ptr(indexName),
+// stringToUTF8Ptr(mapping),
+// )
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 5. 删除索引 - DeleteIndex
+//func (m *EsDLL) DeleteIndex(indexName string) (string, error) {
+// proc, err := m.dll.FindProc("DeleteIndex")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 DeleteIndex: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(stringToUTF8Ptr(indexName))
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 6. 获取索引文档数量 - GetDocumentCount
+//func (m *EsDLL) GetDocumentCount(indexName string) (string, error) {
+// proc, err := m.dll.FindProc("GetDocumentCount")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 GetDocumentCount: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(stringToUTF8Ptr(indexName))
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 7. 创建文档 - CreateDocument
+//func (m *EsDLL) CreateDocument(indexName, id, doc string) (string, error) {
+// proc, err := m.dll.FindProc("CreateDocument")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 CreateDocument: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(
+// stringToUTF8Ptr(indexName),
+// stringToUTF8Ptr(id),
+// stringToUTF8Ptr(doc),
+// )
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 8. 根据ID获取文档 - GetDocument
+//func (m *EsDLL) GetDocument(indexName, id string) (string, error) {
+// proc, err := m.dll.FindProc("GetDocument")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 GetDocument: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(
+// stringToUTF8Ptr(indexName),
+// stringToUTF8Ptr(id),
+// )
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 9. 更新文档 - UpdateDocument
+//func (m *EsDLL) UpdateDocument(indexName, id, updateData string) (string, error) {
+// proc, err := m.dll.FindProc("UpdateDocument")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 UpdateDocument: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(
+// stringToUTF8Ptr(indexName),
+// stringToUTF8Ptr(id),
+// stringToUTF8Ptr(updateData),
+// )
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 10. 删除文档 - DeleteDocument
+//func (m *EsDLL) DeleteDocument(indexName, id string) (string, error) {
+// proc, err := m.dll.FindProc("DeleteDocument")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 DeleteDocument: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(
+// stringToUTF8Ptr(indexName),
+// stringToUTF8Ptr(id),
+// )
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// 11. 搜索文档 - SearchDocuments
+//func (m *EsDLL) SearchDocuments(indexName, query string) (string, error) {
+// proc, err := m.dll.FindProc("SearchDocuments")
+// if err != nil {
+// return "", fmt.Errorf("找不到函数 SearchDocuments: %v", err)
+// }
+// resultPtr, _, _ := proc.Call(
+// stringToUTF8Ptr(indexName),
+// stringToUTF8Ptr(query),
+// )
+// result := m.cStr(resultPtr)
+// return result, nil
+//}
+//
+//// Close 关闭DLL句柄
+//func (m *EsDLL) Close() error {
+// if m.dll != nil {
+// return m.dll.Release()
+// }
+// return nil
+//}
diff --git a/es/es_search.go b/es/es_search.go
new file mode 100644
index 0000000..069fd12
--- /dev/null
+++ b/es/es_search.go
@@ -0,0 +1,3740 @@
+package es
+
+import (
+ "bufio"
+ "bytes"
+ "centerBook/image"
+ "centerBook/kongfz"
+ "centerBook/tail"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ jsoniter "github.com/json-iterator/go"
+
+ "github.com/elastic/go-elasticsearch/v8/esapi"
+)
+
+const ESIndex = "books-from-mysql-v2"
+
+// ESBookResponse 用于返回给Java客户端的格式,ID为简单的int64
+type ESBookResponse struct {
+ ID int64 `json:"id"`
+ BookName string `json:"book_name"`
+ BookPic map[string]interface{} `json:"book_pic"`
+ BookPicS map[string]interface{} `json:"book_pic_s"`
+ BookPicB string `json:"book_pic_b"`
+ BookPicW map[string]interface{} `json:"book_pic_w"`
+ BookDefPic map[string]interface{} `json:"book_def_pic"` // 自制官图,临时用
+ ISBN string `json:"isbn"`
+ Author string `json:"author"`
+ Category string `json:"category"`
+ Publisher string `json:"publisher"`
+ PublicationTime string `json:"publication_time"`
+ BindingLayout string `json:"binding_layout"`
+ FixPrice float64 `json:"fix_price"`
+ Content string `json:"content"`
+ IsSuit int `json:"is_suit"`
+ DaySale7 int `json:"day_sale_7"`
+ DaySale15 int `json:"day_sale_15"`
+ DaySale30 int `json:"day_sale_30"`
+ DaySale60 int `json:"day_sale_60"`
+ DaySale90 int `json:"day_sale_90"`
+ DaySale180 int `json:"day_sale_180"`
+ DaySale365 int `json:"day_sale_365"`
+ ThisYearSale int `json:"this_year_sale"`
+ LastYearSale int `json:"last_year_sale"`
+ TotalSale int `json:"total_sale"`
+ BuyCounts int64 `json:"buy_counts"`
+ SellCounts int64 `json:"sell_counts"`
+ BookPicObj map[string]interface{} `json:"book_pic_obj"`
+ BookPicObjS map[string]interface{} `json:"book_pic_obj_s"`
+ UpdateTime NumberOrString `json:"update_time"`
+ IsIllegal int `json:"is_illegal"` // 是否非法 示例 000000
+ IsReturn int `json:"is_return"` // 是否为驳回 示例 0 否 1 是
+ IsFilter string `json:"is_filter"` // 过滤字段
+}
+
+// FlexibleString 处理可能是字符串或数组的字段
+type FlexibleString struct {
+ Value string
+}
+
+// NewFlexibleString 创建一个FlexibleString实例
+func NewFlexibleString(value string) FlexibleString {
+ return FlexibleString{Value: value}
+}
+
+func (f *FlexibleString) UnmarshalJSON(data []byte) error {
+ // 尝试解析为单个字符串
+ var single string
+ if err := json.Unmarshal(data, &single); err == nil {
+ f.Value = single
+ return nil
+ }
+
+ // 尝试解析为数组
+ var arr []string
+ if err := json.Unmarshal(data, &arr); err == nil {
+ if len(arr) > 0 {
+ f.Value = arr[0] // 取第一个值
+ }
+ return nil
+ }
+
+ return fmt.Errorf("cannot parse string: %s", string(data))
+}
+
+// MarshalJSON 实现JSON序列化,返回字符串而不是对象
+func (f *FlexibleString) MarshalJSON() ([]byte, error) {
+ log.Printf("[FlexibleString MarshalJSON] 正在序列化: %s", f.Value)
+ return json.Marshal(f.Value)
+}
+
+// FlexibleID 处理ID字段可能是数组或单个值的情况
+type FlexibleID struct {
+ Value int64
+}
+
+// NewFlexibleID 创建一个FlexibleID实例
+func NewFlexibleID(value int64) FlexibleID {
+ return FlexibleID{Value: value}
+}
+
+func (f *FlexibleID) UnmarshalJSON(data []byte) error {
+ // 尝试解析为单个数字
+ var single int64
+ if err := json.Unmarshal(data, &single); err == nil {
+ f.Value = single
+ return nil
+ }
+
+ // 尝试解析为数组
+ var arr []int64
+ if err := json.Unmarshal(data, &arr); err == nil {
+ if len(arr) > 0 {
+ f.Value = arr[0] // 取第一个值
+ }
+ return nil
+ }
+
+ // 尝试解析为字符串数字
+ var str string
+ if err := json.Unmarshal(data, &str); err == nil {
+ if val, err := strconv.ParseInt(str, 10, 64); err == nil {
+ f.Value = val
+ return nil
+ }
+ }
+
+ return fmt.Errorf("cannot parse ID: %s", string(data))
+}
+
+// ConvertToResponse 将ESBook转换为ESBookResponse,处理ID字段转换
+func (book *ESBook) ConvertToResponse() ESBookResponse {
+ bookPicMap := map[string]interface{}{
+ "localPath": book.BookPic.LocalPath,
+ "pddPath": book.BookPic.PddPath,
+ }
+ bookPicSMap := map[string]interface{}{
+ "localPath": book.BookPicS.LocalPath,
+ "pddResponse": book.BookPicS.PddResponse,
+ }
+ bookDefPic := map[string]interface{}{
+ "localPath": book.BookDefPic.LocalPath,
+ "pddPath": book.BookDefPic.PddPath,
+ }
+ return ESBookResponse{
+ ID: book.ID,
+ BookName: book.BookName.Value,
+ BookPic: bookPicMap,
+ BookPicS: bookPicSMap,
+ BookPicB: book.BookPicB,
+ BookPicW: book.BookPicW,
+ BookDefPic: bookDefPic,
+ ISBN: book.ISBN,
+ Author: book.Author,
+ Category: book.Category,
+ Publisher: book.Publisher,
+ PublicationTime: book.PublicationTime,
+ BindingLayout: book.BindingLayout,
+ FixPrice: float64(book.FixPrice),
+ Content: book.Content,
+ IsSuit: book.IsSuit,
+ DaySale7: book.DaySale7,
+ DaySale15: book.DaySale15,
+ DaySale30: book.DaySale30,
+ DaySale60: book.DaySale60,
+ DaySale90: book.DaySale90,
+ DaySale180: book.DaySale180,
+ DaySale365: book.DaySale365,
+ ThisYearSale: book.ThisYearSale,
+ LastYearSale: book.LastYearSale,
+ TotalSale: book.TotalSale,
+ BuyCounts: book.BuyCounts,
+ SellCounts: book.SellCounts,
+ BookPicObj: book.BookPicObj,
+ BookPicObjS: book.BookPicObjS,
+ UpdateTime: book.UpdateTime,
+ IsIllegal: book.IsIllegal,
+ IsReturn: book.IsReturn,
+ IsFilter: book.IsFilter,
+ }
+}
+
+// 用于 book_pic
+type BookPicObj struct {
+ LocalPath string `json:"localPath"`
+ PddPath string `json:"pddPath"`
+}
+
+// 用于 book_pic_s
+type BookPicSObj struct {
+ LocalPath string `json:"localPath"`
+ PddResponse string `json:"pddResponse"`
+}
+
+// 用于 book_def_pic
+type BookDefPicObj struct {
+ LocalPath string `json:"localPath"`
+ PddPath string `json:"pddPath"`
+}
+
+type ESBook struct {
+ ID int64 `json:"id,omitempty"`
+ BookName FlexibleString `json:"book_name,omitempty"`
+ BookPic BookPicObj `json:"book_pic,omitempty"`
+ BookPicS BookPicSObj `json:"book_pic_s,omitempty"`
+ BookPicB string `json:"book_pic_b,omitempty"`
+ BookPicW map[string]interface{} `json:"book_pic_w,omitempty"`
+ // 新增自制默认官图字段
+ BookDefPic BookDefPicObj `json:"book_def_pic,omitempty"`
+ ISBN string `json:"isbn,omitempty"`
+ Author string `json:"author,omitempty"`
+ Category string `json:"category,omitempty"`
+ Publisher string `json:"publisher,omitempty"`
+ PublicationTime string `json:"publication_time,omitempty"`
+ BindingLayout string `json:"binding_layout,omitempty"`
+ FixPrice Float64OrString `json:"fix_price,omitempty"`
+ Content string `json:"content,omitempty"`
+ IsSuit int `json:"is_suit,omitempty"`
+ // ⭐ 新增的字段
+ DaySale7 int `json:"day_sale_7,omitempty"`
+ DaySale15 int `json:"day_sale_15,omitempty"`
+ DaySale30 int `json:"day_sale_30,omitempty"`
+ DaySale60 int `json:"day_sale_60,omitempty"`
+ DaySale90 int `json:"day_sale_90,omitempty"`
+ DaySale180 int `json:"day_sale_180,omitempty"`
+ DaySale365 int `json:"day_sale_365,omitempty"`
+ ThisYearSale int `json:"this_year_sale,omitempty"`
+ LastYearSale int `json:"last_year_sale,omitempty"`
+ TotalSale int `json:"total_sale,omitempty"`
+ BuyCounts int64 `json:"buy_counts,omitempty"`
+ SellCounts int64 `json:"sell_counts,omitempty"`
+ BookPicObj map[string]interface{} `json:"book_pic_obj,omitempty"`
+ BookPicObjS map[string]interface{} `json:"book_pic_obj_s,omitempty"`
+ UpdateTime NumberOrString `json:"update_time,omitempty"`
+ IsIllegal int `json:"is_illegal,omitempty"` // 是否非法 示例 000000
+ IsReturn int `json:"is_return,omitempty"` // 是否为驳回 示例 0 否 1 是
+ IsFilter string `json:"is_filter,omitempty"` // 过滤字段
+}
+
+// AddBookRequest 用于 Service 方法的入参
+type AddBookRequest struct {
+ BookName string `json:"book_name"`
+ Author string `json:"author"`
+ Publisher string `json:"publisher"`
+ ISBN string `json:"isbn"`
+ BookPic string `json:"book_pic"`
+ BookPicS string `json:"book_pic_s"`
+ BookPicNew string `json:"Book_pic_new"`
+ BindingLayout string `json:"binding_layout"`
+ FixPrice float64 `json:"fix_price"`
+ PublicationTime string `json:"publication_time"`
+}
+
+// AddBookFullRequest 新的完整插入请求结构体,book_name强制为string类型
+type AddBookFullRequest struct {
+ BookName string `json:"book_name"` // 强制要求string类型
+ BookPic map[string]interface{} `json:"book_pic"` // 支持复杂对象
+ BookPicS map[string]interface{} `json:"book_pic_s"` // 支持复杂对象
+ BookPicB string `json:"book_pic_b"` // 大图地址
+ BookPicW map[string]interface{} `json:"book_pic_w"` // 水印图
+ ISBN string `json:"isbn"` // ISBN号
+ Author string `json:"author"` // 作者
+ Category string `json:"category"` // 分类
+ Publisher string `json:"publisher"` // 出版社
+ PublicationTime string `json:"publication_time"` // 出版时间
+ BindingLayout string `json:"binding_layout"` // 装帧
+ FixPrice float64 `json:"fix_price"` // 定价(分为单位)
+ Content string `json:"content"` // 内容描述
+ IsSuit int `json:"is_suit"` // 是否套装: 0否, 1是
+ DaySale7 int `json:"day_sale_7"` // 7天销量
+ DaySale15 int `json:"day_sale_15"` // 15天销量
+ DaySale30 int `json:"day_sale_30"` // 30天销量
+ DaySale60 int `json:"day_sale_60"` // 60天销量
+ DaySale90 int `json:"day_sale_90"` // 90天销量
+ DaySale180 int `json:"day_sale_180"` // 180天销量
+ DaySale365 int `json:"day_sale_365"` // 365天销量
+ ThisYearSale int `json:"this_year_sale"` // 今年销量
+ LastYearSale int `json:"last_year_sale"` // 去年销量
+ TotalSale int `json:"total_sale"` // 总销量
+ BuyCounts int64 `json:"buy_counts"` // 购买次数
+ SellCounts int64 `json:"sell_counts"` // 售卖次数
+ BookPicObj map[string]interface{} `json:"book_pic_obj"` // 图片对象
+ BookPicObjS map[string]interface{} `json:"book_pic_obj_s"` // 小图对象
+ IsIllegal int `json:"is_illegal"` // 是否非法 示例 000000
+ IsReturn int `json:"is_return"` // 是否为驳回 示例 0 否 1 是
+ IsFilter string `json:"is_filter"` // 过滤字段
+}
+
+type ESSearchService struct {
+ ES *ESClient
+}
+type NumberOrString string
+
+func (n *NumberOrString) UnmarshalJSON(data []byte) error {
+ s := strings.TrimSpace(string(data))
+
+ // 如果是数字(不以引号开头)
+ if len(s) > 0 && s[0] != '"' {
+ *n = NumberOrString(s)
+ return nil
+ }
+
+ // 普通字符串
+ var str string
+ if err := json.Unmarshal(data, &str); err != nil {
+ return err
+ }
+ *n = NumberOrString(str)
+ return nil
+}
+
+type Float64OrString float64
+
+func (f *Float64OrString) UnmarshalJSON(b []byte) error {
+ // 去掉空值
+ if string(b) == "null" || len(b) == 0 {
+ *f = 0
+ return nil
+ }
+
+ // 尝试解析为 float
+ var num float64
+ if err := json.Unmarshal(b, &num); err == nil {
+ *f = Float64OrString(num)
+ return nil
+ }
+
+ // 尝试解析为 string
+ var str string
+ if err := json.Unmarshal(b, &str); err == nil {
+ if str == "" {
+ *f = 0
+ return nil
+ }
+ parsed, err := strconv.ParseFloat(str, 64)
+ if err != nil {
+ return err
+ }
+ *f = Float64OrString(parsed)
+ return nil
+ }
+
+ return fmt.Errorf("无法解析 fix_price: %s", string(b))
+}
+
+func NewESSearchService(es *ESClient) *ESSearchService {
+ return &ESSearchService{ES: es}
+}
+
+// validateBookPicObj 验证 BookPic 对象格式
+func validateBookPicObj(bookPic map[string]interface{}, fieldName string) error {
+ // 允许book_pic为nil,直接返回成功
+ if bookPic == nil {
+ return nil
+ }
+
+ // 检查字段是否存在,不存在则使用默认空字符串
+ localPath, hasLocalPath := bookPic["localPath"]
+ if !hasLocalPath {
+ localPath = ""
+ bookPic["localPath"] = ""
+ }
+
+ pddPath, hasPddPath := bookPic["pddPath"]
+ if !hasPddPath {
+ pddPath = ""
+ bookPic["pddPath"] = ""
+ }
+
+ // 检查字段类型,允许空字符串,但必须是字符串类型
+ if _, ok := localPath.(string); !ok {
+ return fmt.Errorf("%s localPath 必须为字符串类型", fieldName)
+ }
+
+ if _, ok := pddPath.(string); !ok {
+ return fmt.Errorf("%s pddPath 必须为字符串类型", fieldName)
+ }
+
+ return nil
+}
+
+// validateBookPicSObj 验证 BookPicS 对象格式
+func validateBookPicSObj(bookPicS map[string]interface{}, fieldName string) error {
+ // 允许book_pic_s为nil,直接返回成功
+ if bookPicS == nil {
+ return nil
+ }
+
+ // 检查字段是否存在,不存在则使用默认空字符串
+ localPath, hasLocalPath := bookPicS["localPath"]
+ if !hasLocalPath {
+ localPath = ""
+ bookPicS["localPath"] = ""
+ }
+
+ pddResponse, hasPddResponse := bookPicS["pddResponse"]
+ if !hasPddResponse {
+ pddResponse = ""
+ bookPicS["pddResponse"] = ""
+ }
+
+ // 检查字段类型,允许空字符串,但必须是字符串类型
+ if _, ok := localPath.(string); !ok {
+ return fmt.Errorf("%s localPath 必须为字符串类型", fieldName)
+ }
+
+ if _, ok := pddResponse.(string); !ok {
+ return fmt.Errorf("%s pddResponse 必须为字符串类型", fieldName)
+ }
+
+ return nil
+}
+
+// validateBookPicWObj 验证 BookPicW 对象格式
+func validateBookPicWObj(bookPicW map[string]interface{}, fieldName string) error {
+ if bookPicW == nil {
+ return fmt.Errorf("%s 不能为 nil", fieldName)
+ }
+
+ // BookPicW 是任意对象,只需要验证不是空map
+ if len(bookPicW) == 0 {
+ return fmt.Errorf("%s 不能为空对象", fieldName)
+ }
+
+ return nil
+}
+
+// validateBookPicObjS 验证 BookPicObjS 对象格式(类似于 BookPicS)
+func validateBookPicObjS(bookPicObjS map[string]interface{}, fieldName string) error {
+ return validateBookPicSObj(bookPicObjS, fieldName)
+}
+
+type esHitsWrapper struct {
+ Hits struct {
+ Total struct {
+ Value int `json:"value"`
+ } `json:"total"`
+ Hits []struct {
+ Index string `json:"_index"`
+ ID string `json:"_id"`
+ Source ESBook `json:"_source"`
+ } `json:"hits"`
+ } `json:"hits"`
+}
+
+// SearchBooks 搜索图书
+func (svc *ESSearchService) SearchBooks(keyword string) ([]ESBook, error) {
+
+ keyword = strings.TrimSpace(keyword)
+ if keyword == "" {
+ return []ESBook{}, nil
+ }
+
+ query := map[string]interface{}{
+ "_source": []string{
+ "id", "book_name", "book_pic", "book_pic_s",
+ "isbn", "author", "category", "publisher",
+ "publication_time", "binding_layout", "fix_price",
+ "content", "update_time",
+ },
+ "query": map[string]interface{}{
+ "multi_match": map[string]interface{}{
+ "query": keyword,
+ "type": "best_fields",
+ "fields": []string{"book_name^5", "isbn^10", "author^3"},
+ "fuzziness": "AUTO",
+ },
+ },
+ }
+
+ body, err := json.Marshal(query)
+ if err != nil {
+ return nil, fmt.Errorf("序列化请求失败: %v", err)
+ }
+
+ req := esapi.SearchRequest{
+ Index: []string{ESIndex},
+ Body: bytes.NewReader(body),
+ TrackTotalHits: true,
+ }
+
+ res, err := req.Do(context.Background(), svc.ES.Client.Transport)
+ if err != nil {
+ return nil, fmt.Errorf("执行 ES 查询失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return nil, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ var parsed esHitsWrapper
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ return nil, fmt.Errorf("解析 ES 响应失败: %v", err)
+ }
+
+ list := make([]ESBook, 0, len(parsed.Hits.Hits))
+ for _, hit := range parsed.Hits.Hits {
+ list = append(list, hit.Source)
+ }
+
+ return list, nil
+}
+
+// SearchBookByISBN 通过 ISBN 查询图书
+func (svc *ESSearchService) SearchBookByISBN(isbn string) (*ESBook, error) {
+ isbn = strings.TrimSpace(isbn)
+ log.Printf("[SearchBookByISBN] 开始查询 | ISBN=%s", isbn)
+
+ if isbn == "" {
+ log.Printf("[SearchBookByISBN] ISBN 为空,取消查询")
+ return nil, fmt.Errorf("ISBN 不能为空")
+ }
+
+ query := map[string]interface{}{
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ "isbn": isbn, // 精准匹配
+ },
+ },
+ "_source": true,
+ }
+
+ body, err := json.Marshal(query)
+ if err != nil {
+ log.Printf("[SearchBookByISBN] 构建查询 JSON 失败: %v", err)
+ return nil, fmt.Errorf("构建查询 JSON 失败: %v", err)
+ }
+
+ // 执行查询
+ res, err := svc.ES.Client.Search(
+ svc.ES.Client.Search.WithIndex(ESIndex),
+ svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
+ svc.ES.Client.Search.WithTrackTotalHits(true),
+ )
+ if err != nil {
+ log.Printf("[SearchBookByISBN] ES 查询失败: %v", err)
+ return nil, fmt.Errorf("ES 查询失败: %v", err)
+ }
+ defer res.Body.Close()
+ //log.Printf("[SearchBookByISBN] ES 响应: %s", res.String())
+ if res.IsError() {
+ log.Printf("[SearchBookByISBN] ES 返回错误: %s", res.String())
+ return nil, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ var parsed esHitsWrapper
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ log.Printf("[SearchBookByISBN] 解析 ES 返回失败: %v", err)
+ return nil, fmt.Errorf("解析 ES 返回失败: %v", err)
+ }
+
+ if len(parsed.Hits.Hits) == 0 {
+ log.Printf("[SearchBookByISBN] 未找到 ISBN=%s 对应文档", isbn)
+ return nil, nil
+ }
+
+ // 取第一条结果
+ book := parsed.Hits.Hits[0].Source
+ //log.Printf("[SearchBookByISBN] 查询到文档: %+v", book)
+
+ return &book, nil
+}
+
+func (svc *ESSearchService) SearchBookByBookName(bookName string) ([]ESBook, error) {
+
+ bookName = strings.TrimSpace(bookName)
+ if bookName == "" {
+ return nil, fmt.Errorf("bookName 不能为空")
+ }
+
+ query := map[string]interface{}{
+ "_source": true,
+ "query": map[string]interface{}{
+ "match": map[string]interface{}{
+ "book_name": map[string]interface{}{
+ "query": bookName,
+ "operator": "and",
+ "fuzziness": "AUTO",
+ },
+ },
+ },
+ }
+
+ body, _ := json.Marshal(query)
+
+ res, err := svc.ES.Client.Search(
+ svc.ES.Client.Search.WithIndex(ESIndex),
+ svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
+ svc.ES.Client.Search.WithTrackTotalHits(true),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("ES 查询失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return nil, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ var parsed esHitsWrapper
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ return nil, fmt.Errorf("解析 ES 返回失败: %v", err)
+ }
+
+ list := make([]ESBook, 0, len(parsed.Hits.Hits))
+ for _, hit := range parsed.Hits.Hits {
+ list = append(list, hit.Source)
+ }
+
+ return list, nil
+}
+
+// SearchBooksAllFields 单字段匹配
+func (svc *ESSearchService) SearchBooksAllFields(keyword string) ([]ESBook, error) {
+
+ keyword = strings.TrimSpace(keyword)
+ if keyword == "" {
+ return []ESBook{}, nil
+ }
+
+ query := map[string]interface{}{
+ "_source": true,
+ "query": map[string]interface{}{
+ "multi_match": map[string]interface{}{
+ "query": keyword,
+ "type": "best_fields", // 使用最佳字段评分模式
+ "fields": []string{
+ "id^1",
+ "book_name^5",
+ "book_pic^1",
+ "book_pic_s^1",
+ "isbn^10",
+ "author^4",
+ "category^2",
+ "publisher^3",
+ "publication_time^1",
+ "binding_layout^1",
+ "fix_price^1",
+ "content^1",
+ "update_time^1",
+ },
+ "fuzziness": "AUTO", // 自动纠错/模糊匹配
+ },
+ },
+ }
+
+ body, _ := json.Marshal(query)
+
+ res, err := svc.ES.Client.Search(
+ svc.ES.Client.Search.WithIndex(ESIndex),
+ svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
+ svc.ES.Client.Search.WithTrackTotalHits(true),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("执行 ES 查询失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return nil, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ var parsed esHitsWrapper
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ return nil, fmt.Errorf("解析 ES 响应失败: %v", err)
+ }
+
+ list := make([]ESBook, 0, len(parsed.Hits.Hits))
+ for _, hit := range parsed.Hits.Hits {
+ list = append(list, hit.Source)
+ }
+
+ return list, nil
+}
+
+// SearchBooksByConditions 多字段AND搜索
+func (svc *ESSearchService) SearchBooksByConditions(conds map[string]string, page, pageSize int) ([]ESBook, int, error) {
+ mustQueries := make([]map[string]interface{}, 0)
+
+ for field, value := range conds {
+ value = strings.TrimSpace(value)
+ if value == "" {
+ continue
+ }
+
+ // 特殊处理 day_sale_30 做范围查询
+ if field == "day_sale_30" {
+ // 这里假设前端传入 "1",表示查询 >1
+ if v, err := strconv.Atoi(value); err == nil {
+ mustQueries = append(mustQueries, map[string]interface{}{
+ "range": map[string]interface{}{
+ "day_sale_30": map[string]interface{}{
+ "gt": v,
+ },
+ },
+ })
+ }
+ continue
+ }
+
+ // 其他字段用 match 查询
+ mustQueries = append(mustQueries, map[string]interface{}{
+ "match": map[string]interface{}{
+ field: map[string]interface{}{
+ "query": value,
+ "operator": "and",
+ "fuzziness": "AUTO",
+ },
+ },
+ })
+ }
+
+ if len(mustQueries) == 0 {
+ return []ESBook{}, 0, nil
+ }
+
+ from := (page - 1) * pageSize
+
+ query := map[string]interface{}{
+ "_source": true,
+ "from": from,
+ "size": pageSize,
+ "query": map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must": mustQueries,
+ },
+ },
+ }
+
+ body, _ := json.Marshal(query)
+
+ res, err := svc.ES.Client.Search(
+ svc.ES.Client.Search.WithIndex(ESIndex),
+ svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
+ svc.ES.Client.Search.WithTrackTotalHits(true),
+ )
+ if err != nil {
+ return nil, 0, fmt.Errorf("ES 查询失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return nil, 0, fmt.Errorf("ES 错误: %s", res.String())
+ }
+
+ var parsed esHitsWrapper
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ return nil, 0, fmt.Errorf("解析失败: %v", err)
+ }
+
+ list := make([]ESBook, 0, len(parsed.Hits.Hits))
+ for _, hit := range parsed.Hits.Hits {
+ list = append(list, hit.Source)
+ }
+
+ return list, parsed.Hits.Total.Value, nil
+}
+
+// CountByIDRange 统计 ID 范围内的数量
+func (svc *ESSearchService) CountByIDRange(minID, maxID int) (int, error) {
+
+ query := map[string]interface{}{
+ "query": map[string]interface{}{
+ "range": map[string]interface{}{
+ "id": map[string]interface{}{
+ "gte": minID,
+ "lte": maxID,
+ },
+ },
+ },
+ }
+
+ body, _ := json.Marshal(query)
+
+ res, err := svc.ES.Client.Count(
+ svc.ES.Client.Count.WithIndex(ESIndex),
+ svc.ES.Client.Count.WithBody(bytes.NewReader(body)),
+ )
+ if err != nil {
+ return 0, err
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return 0, fmt.Errorf("ES 错误: %s", res.String())
+ }
+
+ var resp struct {
+ Count int `json:"count"`
+ }
+
+ if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
+ return 0, err
+ }
+
+ return resp.Count, nil
+}
+
+// UpdateBookPicByISBN 根据 ISBN 更新 ES 文档的图片字段
+// 功能:更新 `book_pic` 与 `book_pic_s` 字段(任意一个或两个)
+// 入参:
+// - isbn: 目标文档的 ISBN(精确匹配)
+// - bookPic: 新的大图地址(可为空,空则不更新该字段)
+// - bookPicS: 新的小图地址(可为空,空则不更新该字段)
+// 返回:成功更新的文档数量
+func (svc *ESSearchService) UpdateBookPicByISBN(isbn, bookPic, bookPicS string) (int, error) {
+ isbn = strings.TrimSpace(isbn)
+ if isbn == "" {
+ return 0, fmt.Errorf("ISBN 不能为空")
+ }
+ // 构建脚本:仅对提供的字段进行更新
+ scriptParts := make([]string, 0, 2)
+ if strings.TrimSpace(bookPic) != "" {
+ scriptParts = append(scriptParts, "ctx._source.book_pic = params.book_pic;")
+ }
+ if strings.TrimSpace(bookPicS) != "" {
+ scriptParts = append(scriptParts, "ctx._source.book_pic_s = params.book_pic_s;")
+ }
+ if len(scriptParts) == 0 {
+ return 0, fmt.Errorf("至少提供 book_pic 或 book_pic_s 其中之一")
+ }
+
+ source := strings.Join(scriptParts, " ")
+
+ body := map[string]interface{}{
+ "script": map[string]interface{}{
+ "source": source,
+ "lang": "painless",
+ "params": map[string]interface{}{
+ "book_pic": bookPic,
+ "book_pic_s": bookPicS,
+ },
+ },
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ "isbn": isbn,
+ },
+ },
+ }
+
+ payload, _ := json.Marshal(body)
+
+ res, err := svc.ES.Client.UpdateByQuery(
+ []string{ESIndex},
+ svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)),
+ svc.ES.Client.UpdateByQuery.WithRefresh(true),
+ svc.ES.Client.UpdateByQuery.WithConflicts("proceed"),
+ )
+ if err != nil {
+ return 0, fmt.Errorf("ES UpdateByQuery 执行失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return 0, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ // 解析更新数量
+ var parsed struct {
+ Total int `json:"total"`
+ Updated int `json:"updated"`
+ VersionConflicts int `json:"version_conflicts"`
+ }
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ return 0, fmt.Errorf("解析 ES UpdateByQuery 响应失败: %v", err)
+ }
+
+ return parsed.Updated, nil
+}
+
+// UpdateSellCountsByISBN 根据 ISBN 更新 ES 文档的 sell_counts 字段
+// 功能:将指定 ISBN 的文档字段 `sell_counts` 更新为给定值
+// 入参:
+// - isbn: 精确匹配目标文档的 ISBN
+// - sellCounts: 在售数量(非负整数)
+// 返回:成功更新的文档数量
+func (svc *ESSearchService) UpdateSellCountsByISBN(isbn string, sellCounts int) (int, error) {
+ isbn = strings.TrimSpace(isbn)
+ if isbn == "" {
+ return 0, fmt.Errorf("ISBN 不能为空")
+ }
+ if sellCounts < 0 {
+ return 0, fmt.Errorf("sell_counts 不可为负数")
+ }
+
+ body := map[string]interface{}{
+ "script": map[string]interface{}{
+ "source": "ctx._source.sell_counts = params.sell_counts;",
+ "lang": "painless",
+ "params": map[string]interface{}{
+ "sell_counts": sellCounts,
+ },
+ },
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ "isbn": isbn,
+ },
+ },
+ }
+
+ payload, _ := json.Marshal(body)
+
+ res, err := svc.ES.Client.UpdateByQuery(
+ []string{ESIndex},
+ svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)),
+ svc.ES.Client.UpdateByQuery.WithRefresh(true),
+ svc.ES.Client.UpdateByQuery.WithConflicts("proceed"),
+ )
+ if err != nil {
+ return 0, fmt.Errorf("ES UpdateByQuery 执行失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return 0, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ var parsed struct {
+ Total int `json:"total"`
+ Updated int `json:"updated"`
+ VersionConflicts int `json:"version_conflicts"`
+ }
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ return 0, fmt.Errorf("解析 ES UpdateByQuery 响应失败: %v", err)
+ }
+
+ return parsed.Updated, nil
+}
+
+// UpdateSellCountsByISBNHandler 根据请求的 ISBN 调用外部接口获取在售数量并更新 ES
+// 功能:
+// 1) 读取 `isbn` 参数;
+// 2) 调用 tail.GetOnSaleCount 获取在售数量;
+// 3) 调用 UpdateSellCountsByISBN 更新 ES;
+// 4) 返回更新结果与在售数量。
+func (svc *ESSearchService) UpdateSellCountsByISBNHandler(c *gin.Context) {
+ // 异步执行:立即返回成功,后台更新 ES
+ isbn := strings.TrimSpace(c.Query("isbn"))
+ if isbn == "" {
+ c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}})
+ return
+ }
+
+ // 获取在售数量(容错 data:{})
+ onSaleCount, err := tail.GetOnSaleCount(isbn)
+ log.Printf("[Async] 获取在售数量 | ISBN=%s | count=%d", isbn, onSaleCount)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "获取在售数量失败", "details": err.Error()})
+ return
+ }
+ if onSaleCount == 0 {
+ c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}})
+ return
+ }
+
+ // 后台异步更新
+ go func(isbn string, count int) {
+ updated, err := svc.UpdateSellCountsByISBN(isbn, count)
+ if err != nil {
+ log.Printf("[Async] 更新 ES 失败 | ISBN=%s | err=%v", isbn, err)
+ return
+ }
+ log.Printf("[Async] 更新 ES 成功 | ISBN=%s | sell_counts=%d | updated=%d", isbn, count, updated)
+ }(isbn, onSaleCount)
+
+ // 立即返回成功,携带异步提示
+ c.JSON(200, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": gin.H{"isbn": isbn, "on_sale_count": onSaleCount, "async": true},
+ })
+}
+
+// UpdateSellCountsDirectHandler 直接按入参异步更新 ES 的在售数量
+// 功能:
+// - 从请求中读取 `isbn` 与 `onSaleCount`;
+// - 校验参数(ISBN 非空、onSaleCount >= 0 且为整数);
+// - 后台异步调用 ES 更新 `sell_counts` 字段;
+// - 立即返回标准成功结构,包含 async 提示;当 onSaleCount=0 时直接返回 success 不更新。
+func (svc *ESSearchService) UpdateSellCountsDirectHandler(c *gin.Context) {
+ isbn := strings.TrimSpace(c.Query("isbn"))
+ countStr := strings.TrimSpace(c.Query("onSaleCount"))
+
+ if isbn == "" {
+ c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
+ return
+ }
+ if countStr == "" {
+ c.JSON(400, gin.H{"error": "缺少 onSaleCount 参数"})
+ return
+ }
+
+ count, err := strconv.Atoi(countStr)
+ if err != nil {
+ c.JSON(400, gin.H{"error": "onSaleCount 必须为整数", "details": err.Error()})
+ return
+ }
+ if count < 0 {
+ c.JSON(400, gin.H{"error": "onSaleCount 不可为负数"})
+ return
+ }
+ // 如果为 0,直接返回 success,不做更新
+ if count == 0 {
+ c.JSON(200, gin.H{"code": 200, "message": "success", "data": gin.H{}})
+ return
+ }
+
+ // 后台异步更新 ES 的 sell_counts 字段
+ go func(isbn string, c int) {
+ updated, err := svc.UpdateSellCountsByISBN(isbn, c)
+ if err != nil {
+ log.Printf("[Async] 直接更新 ES 失败 | ISBN=%s | err=%v", isbn, err)
+ return
+ }
+ log.Printf("[Async] 直接更新 ES 成功 | ISBN=%s | sell_counts=%d | updated=%d", isbn, c, updated)
+ }(isbn, count)
+
+ // 立即返回成功,携带异步提示
+ c.JSON(200, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": gin.H{"isbn": isbn, "sell_counts": count, "async": true},
+ })
+}
+
+// SearchBookBaseInfoES 根据条件查询 ES 图书信息
+func (svc *ESSearchService) SearchBookBaseInfoES(c *gin.Context) ([]ESBook, int, error) {
+
+ q := c.Request.URL.Query()
+
+ must := make([]map[string]interface{}, 0)
+
+ // ES 字段映射
+ snakeMap := map[string]string{
+ "bookName": "book_name",
+ "bookPic": "book_pic",
+ "publication_times": "publication_time",
+ "isSuit": "is_suit",
+ }
+
+ // ===== saleSelect 对应字段映射 =====
+ saleSelect := c.DefaultQuery("saleSelect", "2")
+ saleField := map[string]string{
+ "7": "day_sale_7",
+ "15": "day_sale_15",
+ "30": "day_sale_30",
+ "60": "day_sale_60",
+ "90": "day_sale_90",
+ "180": "day_sale_180",
+ "365": "day_sale_365",
+ "0": "this_year_sale",
+ "1": "last_year_sale",
+ }[saleSelect]
+
+ fmt.Printf("[DEBUG] saleSelect=%s saleField=%s\n", saleSelect, saleField)
+
+ // saleField >= 1 默认条件
+ if saleField != "" {
+ cond := map[string]interface{}{
+ "range": map[string]interface{}{
+ saleField: map[string]interface{}{"gte": 1},
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ }
+
+ // per_page → pageSize
+ if perPage := c.Query("per_page"); perPage != "" {
+ q.Set("pageSize", perPage)
+ }
+
+ // ========== 遍历参数 ==========
+
+ for key, vals := range q {
+ val := strings.TrimSpace(vals[0])
+ if val == "" {
+ continue
+ }
+
+ if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" {
+ continue
+ }
+
+ // 忽略无用字段
+ if key == "is_pricing" ||
+ key == "vio_book" ||
+ key == "book_set" ||
+ key == "onenum_mbooks" ||
+ key == "ill_publisher" ||
+ key == "ill_author" {
+ continue
+ }
+
+ originalKey := key
+ fmt.Printf("[DEBUG] Processing key=%s val=%s\n", key, val)
+
+ // 字段映射
+ if nk, ok := snakeMap[key]; ok {
+ key = nk
+ }
+
+ // ===== is_suit =====
+ if key == "is_suit" {
+ if num, err := strconv.Atoi(val); err == nil {
+ cond := map[string]interface{}{
+ "term": map[string]interface{}{"is_suit": num},
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ }
+ continue
+ }
+
+ // ===== categoryType =====
+ if originalKey == "categoryType" {
+ var cond map[string]interface{}
+ if val == "1" {
+ cond = map[string]interface{}{
+ "prefix": map[string]interface{}{"isbn": "9787"},
+ }
+ } else {
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must_not": []map[string]interface{}{
+ {"prefix": map[string]interface{}{"isbn": "9787"}},
+ },
+ },
+ }
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // ===== category =====
+ if key == "category" {
+ var cond map[string]interface{}
+ if val == "排除大学教材" {
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must_not": []map[string]interface{}{
+ {"match_phrase": map[string]interface{}{
+ "category": "图书/教材教辅考试/大学教材",
+ }},
+ },
+ },
+ }
+ } else {
+ cond = map[string]interface{}{
+ "match_phrase": map[string]interface{}{"category": val},
+ }
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // =====================================================
+ // ========== ★★★ book_pic + picType 联动逻辑 ★★★ ==========
+ // =====================================================
+ if key == "book_pic" {
+
+ picType := c.DefaultQuery("picType", "1") // 默认官图
+
+ var targetField string
+ if picType == "1" {
+ targetField = "book_pic.pddPath"
+ } else {
+ targetField = "book_pic_s.pddResponse"
+ }
+
+ var cond map[string]interface{}
+ if val == "1" {
+ // 有图:字段必须存在且非空
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must": []map[string]interface{}{
+ {"exists": map[string]interface{}{"field": targetField}},
+ {"wildcard": map[string]interface{}{
+ targetField: "*",
+ }},
+ },
+ },
+ }
+ } else {
+ // 无图:使用keyword字段查询字段不存在或为空字符串
+ var keywordField string
+ if picType == "1" {
+ keywordField = "book_pic.pddPath.keyword"
+ } else {
+ keywordField = "book_pic_s.pddResponse.keyword"
+ }
+
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "should": []map[string]interface{}{
+ // 情况1:字段完全不存在
+ {
+ "bool": map[string]interface{}{
+ "must_not": []map[string]interface{}{
+ {"exists": map[string]interface{}{"field": keywordField}},
+ },
+ },
+ },
+ // 情况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
+
+ fmt.Printf("[DEBUG] page=%d pageSize=%d from=%d\n", page, pageSize, from)
+
+ // ========== 构建 ES 查询 ==========
+ query := map[string]interface{}{
+ "from": from,
+ "size": pageSize,
+ "query": map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must": must,
+ },
+ },
+ "sort": []map[string]interface{}{
+ {"update_time": map[string]interface{}{"order": "desc"}},
+ },
+ }
+
+ body, _ := json.MarshalIndent(query, "", " ")
+ fmt.Printf("[DEBUG] ES Query Body:\n%s\n", string(body))
+
+ // ========== 执行 ES 查询 ==========
+ res, err := svc.ES.Client.Search(
+ svc.ES.Client.Search.WithIndex(ESIndex),
+ svc.ES.Client.Search.WithBody(bytes.NewReader(body)),
+ svc.ES.Client.Search.WithTrackTotalHits(true),
+ )
+
+ if err != nil {
+ fmt.Printf("[ERROR] ES.Client.Search error: %v\n", err)
+ return nil, 0, err
+ }
+ defer res.Body.Close()
+
+ //if res.IsError() {
+ // raw, _ := io.ReadAll(res.Body)
+ // fmt.Printf("[ERROR] ES error body: %s\n", string(raw))
+ // return nil, 0, fmt.Errorf("ES error: %s", string(raw))
+ //}
+ // 读取响应
+ var buf bytes.Buffer
+ // 使用CopyBuffer可以重用缓冲区
+ writer := bufio.NewWriterSize(&buf, 8192)
+ _, err = io.Copy(writer, res.Body)
+ if err != nil {
+ return nil, 0, fmt.Errorf("复制响应数据失败: %v", err)
+ }
+ rawData := buf.Bytes()
+ //rawData, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, 0, fmt.Errorf("读取响应失败: %v", err)
+ }
+ // 检查是否有数据
+ if len(rawData) == 0 {
+ return nil, 0, fmt.Errorf("ES返回空响应")
+ }
+
+ // 验证是否是有效的JSON
+ if rawData[0] != '{' {
+ return nil, 0, fmt.Errorf("ES返回非JSON响应: %s", string(rawData[:min(100, len(rawData))]))
+ }
+
+ fmt.Println("[DEBUG] ES query executed successfully")
+
+ var _json = jsoniter.ConfigCompatibleWithStandardLibrary
+ var parsed esHitsWrapper
+ if err := _json.Unmarshal(rawData, &parsed); err != nil {
+ return nil, 0, fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, string(rawData[:min(200, len(rawData))]))
+ }
+ //var parsed esHitsWrapper
+ //if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ // fmt.Printf("[ERROR] JSON Decode error: %v\n", err)
+ // return nil, 0, err
+ //}
+ list := make([]ESBook, 0, len(parsed.Hits.Hits))
+ for _, hit := range parsed.Hits.Hits {
+ list = append(list, hit.Source)
+ }
+
+ return list, parsed.Hits.Total.Value, nil
+}
+func (svc *ESSearchService) BatchGetBookBaseInfoES(c *gin.Context) ([]ESBook, int, error) {
+ q := c.Request.URL.Query()
+
+ must := make([]map[string]interface{}, 0)
+
+ // ES 字段映射
+ snakeMap := map[string]string{
+ "bookName": "book_name",
+ "bookPic": "book_pic",
+ "publication_times": "publication_time",
+ "isSuit": "is_suit",
+ }
+
+ // ===== saleSelect 对应字段映射 =====
+ saleSelect := c.DefaultQuery("saleSelect", "")
+ saleField := map[string]string{
+ "7": "day_sale_7",
+ "15": "day_sale_15",
+ "30": "day_sale_30",
+ "60": "day_sale_60",
+ "90": "day_sale_90",
+ "180": "day_sale_180",
+ "365": "day_sale_365",
+ "0": "this_year_sale",
+ "1": "last_year_sale",
+ }[saleSelect]
+
+ fmt.Printf("[DEBUG] saleSelect=%s saleField=%s\n", saleSelect, saleField)
+
+ // saleField >= 1 默认条件
+ if saleField != "" {
+ cond := map[string]interface{}{
+ "range": map[string]interface{}{
+ saleField: map[string]interface{}{"gte": 1},
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ }
+
+ // per_page → pageSize
+ if perPage := c.Query("per_page"); perPage != "" {
+ q.Set("pageSize", perPage)
+ }
+
+ // ========== 遍历参数 ==========
+
+ for key, vals := range q {
+ val := strings.TrimSpace(vals[0])
+ if val == "" {
+ continue
+ }
+ // 不作为查询条件的字段
+ if key == "page" || key == "pageSize" || key == "per_page" || key == "saleSelect" || key == "picType" || key == "shopType" {
+ continue
+ }
+
+ // 忽略无用字段
+ if key == "is_pricing" ||
+ key == "vio_book" ||
+ key == "book_set" ||
+ key == "onenum_mbooks" ||
+ key == "ill_publisher" ||
+ key == "ill_author" {
+ continue
+ }
+
+ originalKey := key
+ fmt.Printf("[DEBUG] Processing key=%s val=%s\n", key, val)
+
+ // 字段映射
+ if nk, ok := snakeMap[key]; ok {
+ key = nk
+ }
+
+ // ===== is_suit =====
+ if key == "is_suit" {
+ fmt.Printf("[DEBUG] is_suit val=%q\n", val)
+ if num, err := strconv.Atoi(val); err == nil {
+ cond := map[string]interface{}{
+ "term": map[string]interface{}{"is_suit": num},
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ } else {
+ fmt.Printf("[ERROR] is_suit Atoi error: %v\n", err)
+ }
+ continue
+ }
+
+ // ===== is_return =====
+ if key == "is_return" {
+ fmt.Printf("[DEBUG] is_return val=%q\n", val)
+ if num, err := strconv.Atoi(val); err == nil {
+ cond := map[string]interface{}{
+ "term": map[string]interface{}{"is_return": num},
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ } else {
+ fmt.Printf("[ERROR] is_return Atoi error: %v\n", err)
+ }
+ continue
+ }
+
+ // ===== is_filter =====
+ // ===== is_filter(按 shopType 位匹配)=====
+ if key == "is_filter" {
+ fmt.Printf("[DEBUG] is_filter val=%q\n", val)
+
+ // 只处理 1(=1) 和 2(=0)
+ if val != "1" && val != "2" {
+ continue
+ }
+
+ shopType := c.DefaultQuery("shopType", "")
+ var pattern string
+
+ // val=1 -> 位=1
+ // val=2 -> 位=0
+ targetBit := "1"
+ if val == "2" {
+ targetBit = "0"
+ }
+
+ switch shopType {
+ case "0":
+ pattern = targetBit + "*"
+ case "1":
+ pattern = "?" + targetBit + "*"
+ case "2":
+ pattern = "??" + targetBit + "*"
+ case "3":
+ pattern = "???" + targetBit + "*"
+ default:
+ continue
+ }
+
+ cond := map[string]interface{}{
+ "wildcard": map[string]interface{}{
+ "is_filter": pattern,
+ },
+ }
+
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // ===== categoryType =====
+ if originalKey == "categoryType" {
+ var cond map[string]interface{}
+ if val == "1" {
+ cond = map[string]interface{}{
+ "prefix": map[string]interface{}{"isbn": "9787"},
+ }
+ } else {
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must_not": []map[string]interface{}{
+ {"prefix": map[string]interface{}{"isbn": "9787"}},
+ },
+ },
+ }
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // ===== category =====
+ if key == "category" {
+ var cond map[string]interface{}
+ if val == "排除大学教材" {
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must_not": []map[string]interface{}{
+ {"match_phrase": map[string]interface{}{
+ "category": "图书/教材教辅考试/大学教材",
+ }},
+ },
+ },
+ }
+ } else {
+ cond = map[string]interface{}{
+ "match_phrase": map[string]interface{}{"category": val},
+ }
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // =====================================================
+ // ========== ★★★ book_pic + picType 联动逻辑 ★★★ ==========
+ // =====================================================
+ if key == "book_pic" {
+
+ picType := c.DefaultQuery("picType", "1") // 默认官图
+
+ var targetField string
+ if picType == "1" {
+ targetField = "book_pic.pddPath"
+ } else {
+ targetField = "book_pic_s.pddResponse"
+ }
+
+ var cond map[string]interface{}
+ if val == "1" {
+ // 有图:字段必须存在且非空
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must": []map[string]interface{}{
+ {"exists": map[string]interface{}{"field": targetField}},
+ {"wildcard": map[string]interface{}{
+ targetField: "*",
+ }},
+ },
+ },
+ }
+ } else {
+ // 无图:使用keyword字段查询字段不存在或为空字符串
+ var keywordField string
+ if picType == "1" {
+ keywordField = "book_pic.pddPath.keyword"
+ } else {
+ keywordField = "book_pic_s.pddResponse.keyword"
+ }
+
+ cond = map[string]interface{}{
+ "bool": map[string]interface{}{
+ "should": []map[string]interface{}{
+ // 情况1:字段完全不存在
+ {
+ "bool": map[string]interface{}{
+ "must_not": []map[string]interface{}{
+ {"exists": map[string]interface{}{"field": keywordField}},
+ },
+ },
+ },
+ // 情况2:keyword字段存在但为空字符串
+ {
+ "term": map[string]interface{}{
+ keywordField: "",
+ },
+ },
+ },
+ "minimum_should_match": 1,
+ },
+ }
+ }
+
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // ===== buy_counts → saleField =====
+ if key == "buy_counts" {
+ fmt.Printf("[DEBUG] buy_counts uses saleField=%s\n", saleField)
+ if saleField == "" {
+ continue
+ }
+
+ parts := strings.Split(val, ",")
+ if len(parts) == 2 {
+ minVal, _ := strconv.Atoi(parts[0])
+ maxVal, _ := strconv.Atoi(parts[1])
+
+ cond := map[string]interface{}{
+ "range": map[string]interface{}{
+ saleField: map[string]interface{}{
+ "gte": minVal,
+ "lte": maxVal,
+ },
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ if num, err := strconv.Atoi(val); err == nil {
+ cond := map[string]interface{}{
+ "range": map[string]interface{}{
+ saleField: map[string]interface{}{
+ "gte": num,
+ },
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+ }
+
+ // ===== totalSale_range =====
+ if key == "totalSale_range" {
+ parts := strings.Split(val, ",")
+ if len(parts) == 2 {
+ minVal, _ := strconv.Atoi(parts[0])
+ maxVal, _ := strconv.Atoi(parts[1])
+ cond := map[string]interface{}{
+ "range": map[string]interface{}{
+ "total_sale": map[string]interface{}{
+ "gte": minVal,
+ "lte": maxVal,
+ },
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+ }
+
+ // ===== 数值范围 =====
+ if key == "sell_counts" ||
+ strings.HasPrefix(key, "day_sale_") ||
+ key == "this_year_sale" || key == "last_year_sale" ||
+ key == "publication_time" {
+
+ parts := strings.Split(val, ",")
+ if len(parts) == 2 {
+ minVal, _ := strconv.Atoi(parts[0])
+ maxVal, _ := strconv.Atoi(parts[1])
+
+ cond := map[string]interface{}{
+ "range": map[string]interface{}{
+ key: map[string]interface{}{
+ "gte": minVal,
+ "lte": maxVal,
+ },
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+ }
+
+ // ===== 精确匹配 =====
+ if key == "isbn" || key == "id" || key == "publisher" {
+ cond := map[string]interface{}{
+ "term": map[string]interface{}{key: val},
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // ===== 模糊匹配 =====
+ if key == "book_name" || key == "author" {
+ cond := map[string]interface{}{
+ "match": map[string]interface{}{
+ key: map[string]interface{}{
+ "query": val,
+ "operator": "and",
+ "fuzziness": "AUTO",
+ },
+ },
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ continue
+ }
+
+ // ===== 默认前缀匹配 =====
+ cond := map[string]interface{}{
+ "prefix": map[string]interface{}{key: val},
+ }
+ must = append(must, cond)
+ fmt.Printf("[DEBUG] must += %v\n", cond)
+ }
+
+ // ========== 分页 ==========
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10")))
+ from := (page - 1) * pageSize
+ // ========== pageSize大于500则根据id查询 =========
+ fmt.Printf("[DEBUG] page=%d pageSize=%d from=%d\n", page, pageSize, from)
+ var sort []map[string]interface{}
+ if pageSize >= 500 {
+ sort = []map[string]interface{}{
+ {"id": map[string]interface{}{"order": "asc"}},
+ }
+ } else {
+ sort = []map[string]interface{}{
+ {"update_time": map[string]interface{}{"order": "desc"}},
+ }
+ }
+ // ========== 构建 ES 查询 ==========
+ query := map[string]interface{}{
+ "from": from,
+ "size": pageSize,
+ "query": map[string]interface{}{
+ "bool": map[string]interface{}{
+ "must": must,
+ },
+ },
+ "sort": sort,
+ "_source": map[string]interface{}{
+ "includes": []string{"isbn"}, // 排除content字段
+ },
+ }
+
+ 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
+}
+
+// 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]string, 0, len(list))
+ for _, book := range list {
+ isbnList = append(isbnList, map[string]string{"isbn": book.ISBN})
+ }
+
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSizeStr := c.DefaultQuery("pageSize", c.DefaultQuery("per_page", "10"))
+ pageSize, _ := strconv.Atoi(pageSizeStr)
+
+ fmt.Printf("[DEBUG] Response Info => total=%d page=%d pageSize=%d returnCount=%d\n",
+ total, page, pageSize, len(isbnList))
+
+ c.JSON(200, gin.H{
+ "current_page": page,
+ "data": isbnList,
+ "per_page": pageSize,
+ "total": total,
+ })
+}
+
+func (svc *ESSearchService) SearchBooksHandler(c *gin.Context) {
+ isbn := c.Query("isbn")
+ if isbn == "" {
+ c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
+ return
+ }
+
+ result, err := svc.SearchBooks(isbn)
+ 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) SearchBookByISBNHandler(c *gin.Context) {
+ // 访问 http://localhost:8095 连接
+ //resp, err := http.Get("http://localhost:8095")
+ //if err != nil {
+ // // 请求失败时处理错误
+ // fmt.Println("Error:", err)
+ // return
+ //}
+ //defer resp.Body.Close() // 确保响应体被关闭
+
+ isbn := c.Query("isbn")
+ if isbn == "" {
+ log.Printf("[SearchBookByISBNHandler] 缺少 isbn 参数")
+ c.JSON(400, gin.H{"error": "缺少 isbn 参数"})
+ return
+ }
+
+ log.Printf("[SearchBookByISBNHandler] 查询 ISBN: %s", isbn)
+
+ // 1. 先从 ES 查询
+ result, err := svc.SearchBookByISBN(isbn)
+ if err != nil {
+ log.Printf("[SearchBookByISBNHandler] ES 查询失败: %v", err)
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ if result == nil {
+ log.Printf("[SearchBookByISBNHandler] ES 中未找到 ISBN: %s,从孔夫子抓取", isbn)
+
+ // 2. ES 没有,从孔夫子获取
+ 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)
+
+ // 3. 下载并上传 book_pic
+ 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)
+ }
+ }
+
+ // 4. 下载并上传 book_pic_s
+ 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)
+ }
+ }
+
+ // 5. 转换为 ESBook
+ esBook := ConvertKongfzToESBook(apiBook)
+
+ // 替换 BookPicS 和 BookPic 的 PDD URL
+ esBook.BookPicS.PddResponse = pddBookPicSURL
+ esBook.BookPic.PddPath = pddBookPicURL
+ //log.Printf("[SearchBookByISBNHandler] 写入 ES: %+v", esBook)
+
+ // 6. 写入 ES
+ result, err = svc.AddBookToES(c.Request.Context(), esBook)
+ if err != nil {
+ log.Printf("[SearchBookByISBNHandler] 写入 ES 失败: %v", err)
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ log.Printf("[SearchBookByISBNHandler] 写入 ES 成功, ISBN: %s", isbn)
+ } else {
+ //log.Printf("[SearchBookByISBNHandler] 从 ES 查询到图书: %+v", result)
+ }
+
+ // 转换为Java兼容的响应格式
+ responseData := result.ConvertToResponse()
+ c.JSON(200, gin.H{
+ "data": responseData,
+ })
+}
+
+// ConvertKongfzToESBook 将第三方接口返回的数据转换为 ESBook 结构
+func ConvertKongfzToESBook(apiBook *kongfz.BookResponse) *ESBook {
+ if apiBook == nil || apiBook.Data.ISBN == "" {
+ return nil
+ }
+ pubTimeStr := ""
+ if apiBook.Data.PublicationTime != 0 {
+ pubTimeStr = time.Unix(apiBook.Data.PublicationTime, 0).Format("2006-01")
+ }
+ // 处理 fix_price -> 分为单位
+ fixPriceFen := 0
+ if apiBook.Data.FixPrice != "" {
+ priceFloat, err := strconv.ParseFloat(apiBook.Data.FixPrice, 64)
+ if err == nil {
+ fixPriceFen = int(priceFloat * 100) // 元 -> 分
+ }
+ }
+ return &ESBook{
+ ISBN: apiBook.Data.ISBN,
+ BookName: FlexibleString{Value: apiBook.Data.BookName},
+ Author: apiBook.Data.Author,
+ Publisher: apiBook.Data.Publisher,
+ PublicationTime: pubTimeStr,
+ FixPrice: Float64OrString(float64(fixPriceFen)),
+ BookPic: BookPicObj{
+ LocalPath: "",
+ PddPath: apiBook.Data.BookPic, // 初始化 BookPic 字段
+ },
+ BookPicS: BookPicSObj{
+ LocalPath: "",
+ PddResponse: apiBook.Data.BookPicS,
+ },
+ BookPicObjS: map[string]interface{}{
+ "pddResponse": apiBook.Data.BookPicS,
+ },
+ // 其他字段默认赋值
+ BuyCounts: 0,
+ SellCounts: 0,
+ TotalSale: 0,
+ BookPicW: make(map[string]interface{}),
+ IsSuit: 0,
+ Category: "",
+ Content: "",
+ }
+}
+
+func (svc *ESSearchService) SearchBookByBookNameHandler(c *gin.Context) {
+ bookName := c.Query("bookName")
+ if bookName == "" {
+ c.JSON(400, gin.H{"error": "缺少 bookName 参数"})
+ return
+ }
+
+ result, err := svc.SearchBookByBookName(bookName)
+ if err != nil {
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ if len(result) == 0 {
+ c.JSON(404, gin.H{"error": "未找到该书名的图书"})
+ return
+ }
+
+ // 转换为Java兼容的响应格式
+ responseList := make([]ESBookResponse, 0, len(result))
+ for _, book := range result {
+ responseList = append(responseList, book.ConvertToResponse())
+ }
+
+ c.JSON(200, gin.H{
+ "count": len(result),
+ "data": responseList,
+ })
+}
+func (svc *ESSearchService) SearchBooksAllFieldsHandler(c *gin.Context) {
+ keyword := c.Query("q")
+ if keyword == "" {
+ c.JSON(400, gin.H{"error": "缺少搜索参数 q"})
+ return
+ }
+
+ result, err := svc.SearchBooksAllFields(keyword)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "ES 查询失败", "details": err.Error()})
+ return
+ }
+
+ // 转换为Java兼容的响应格式
+ responseList := make([]ESBookResponse, 0, len(result))
+ for _, book := range result {
+ responseList = append(responseList, book.ConvertToResponse())
+ }
+
+ c.JSON(200, gin.H{
+ "count": len(result),
+ "data": responseList,
+ })
+}
+func (svc *ESSearchService) SearchBooksByConditionsHandler(c *gin.Context) {
+ conds := map[string]string{
+ "book_name": c.Query("book_name"),
+ "isbn": c.Query("isbn"),
+ "author": c.Query("author"),
+ "category": c.Query("category"),
+ "publisher": c.Query("publisher"),
+ "publication_time": c.Query("publication_time"),
+ "day_sale_30": c.Query("day_sale_30"),
+ }
+
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
+
+ result, total, err := svc.SearchBooksByConditions(conds, page, pageSize)
+ if err != nil {
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 转换为Java兼容的响应格式
+ responseList := make([]ESBookResponse, 0, len(result))
+ for _, book := range result {
+ responseList = append(responseList, book.ConvertToResponse())
+ }
+
+ c.JSON(200, gin.H{
+ "count": len(result),
+ "total": total,
+ "data": responseList,
+ "page": page,
+ "pageSize": pageSize,
+ })
+}
+
+// CountByIDRangeHandler Gin 包装器
+func (svc *ESSearchService) CountByIDRangeHandler(c *gin.Context) {
+
+ minID, _ := strconv.Atoi(c.DefaultQuery("minID", "0"))
+ maxID, _ := strconv.Atoi(c.DefaultQuery("maxID", "0"))
+
+ if minID == 0 || maxID == 0 {
+ c.JSON(400, gin.H{
+ "error": "缺少 minID 或 maxID 参数",
+ })
+ return
+ }
+
+ count, err := svc.CountByIDRange(minID, maxID)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "ES 查询失败",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "minID": minID,
+ "maxID": maxID,
+ "count": count,
+ })
+}
+
+// CheckBookSuit 检查书名是否包含套装关键字
+func CheckBookSuit(bookName string) bool {
+ if bookName == "" {
+ return false
+ }
+
+ // 套装关键字列表
+ suitKeywords := []string{
+ "套装", "卷本", "上中下", "上、下",
+ "共二", "共三", "共四", "共五", "共六", "共七", "共八", "共九",
+ "共十", "共十一", "共十二", "共十三", "共十四", "共十五", "共十六",
+ "共二十", "共二十一", "共二十二", "共二十三", "共二十四", "共二十五", "共二十六", "共二十七", "共二十八", "共二十九", "共三十",
+ "共2", "共3", "共4", "共5", "共6", "共7", "共8", "共9",
+ "全1", "全2", "全3", "全4", "全5", "全6", "全7", "全八", "全9",
+ "共11", "共12", "共13", "共14", "共15", "共16", "共17", "共18", "共19",
+ "全二", "全三", "全四", "全五", "全六", "全七", "全八", "全九",
+ "2本合售", "3本合售", "4本合售", "5本合售", "6本合售", "7本合售", "8本合售", "9本合售",
+ "合二", "合三", "合四", "合五", "合六", "合七", "合八", "合九", "合十",
+ "合十一", "合十二", "合十三", "合十四", "合十五", "合十六", "合十七", "合十八", "合十九",
+ "合十本",
+ "合十一本", "合十二本", "合十三本", "合十四本", "合十五本", "合",
+ "全贰本", "全叁本", "全肆本", "全伍本", "全陆本", "全柒本", "全捌本", "全玖本", "全拾本",
+ "全拾壹本", "全拾贰本", "全拾叁本", "全拾肆本", "全拾伍本", "全拾陆本", "全拾柒本", "全拾捌本", "全拾玖本",
+ "合2册", "合3册", "合4册", "合5册", "合6册", "合7册", "合8册", "合9册",
+ "合10册", "合11册", "合12册", "合13册", "合14册", "合15册", "合16册", "合17册", "合18册", "合19册",
+ "共2册", "共3册", "共4册", "共5册", "共6册", "共7册", "共8册", "共9册",
+ "共10册", "共11册", "共12册", "共13册", "共14册", "共15册", "共16册", "共17册", "共18册", "共19册",
+ "全2册", "全3册", "全4册", "全5册", "全6册", "全7册", "全8册", "全9册",
+ "全10册", "全11册", "全12册", "全13册", "全14册", "全15册", "全3卷", "全4卷", "全5卷", "全6卷", "全7卷", "全8卷", "全合拾肆卷", "合拾伍卷", "合拾陆卷", "合拾柒卷", "合拾捌卷", "合拾玖卷",
+ "共贰卷", "共叁卷", "共肆卷", "共伍卷", "共陆卷", "共柒卷", "共捌卷", "共玖卷", "共拾卷",
+ "共拾壹卷", "共拾贰卷", "共拾叁卷", "共拾肆卷", "共拾伍卷", "共拾陆卷", "共拾柒卷", "共拾捌卷", "共拾玖卷",
+ "全贰卷", "全叁卷", "全肆卷", "全伍卷", "全陆卷", "全柒卷", "全捌卷", "全玖卷", "全拾卷",
+ "全拾壹卷", "全拾贰卷", "全拾叁卷", "全拾肆卷", "全拾伍卷", "全拾陆卷", "全拾柒卷", "全拾捌卷", "全拾玖卷",
+ "合2辑", "合3辑", "合4辑", "合5辑", "合6辑", "合7辑", "合8辑", "合9辑",
+ "合10辑", "合11辑", "合12辑", "合13辑", "合14辑", "合15辑", "合16辑", "合17辑", "合18辑", "合19辑",
+ "共2辑", "共3辑", "共4辑", "共5辑", "共6辑", "卷", "全12卷", "全13卷", "全14卷", "册", "合四册", "合五册", "合六册", "合七册", "合八册", "合九册", "合十册",
+ "十六本", "合十七本", "合十八本", "合十九本",
+ "共二本", "共三本", "共", "一套八本", "一套九本", "一套十本", "一套十一本",
+ "1-1", "1-2", "1-3", "共十七", "共十八", "共十九",
+ }
+
+ // 检查书名是否包含任何一个套装关键字
+ for _, keyword := range suitKeywords {
+ if strings.Contains(bookName, keyword) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// CheckBookSuitHandler 检查书名是否包含套装关键字的HTTP处理器
+func (svc *ESSearchService) CheckBookSuitHandler(c *gin.Context) {
+ bookName := c.Query("bookName")
+ if bookName == "" {
+ c.JSON(400, gin.H{
+ "error": "缺少 bookName 参数",
+ })
+ return
+ }
+
+ isSuit := CheckBookSuit(bookName)
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": gin.H{
+ "book_name": bookName,
+ "is_suit": isSuit,
+ },
+ })
+}
+
+// UpdateBookSuitByISBNHandler 根据ISBN更新图书的is_suit字段
+func (svc *ESSearchService) UpdateBookSuitByISBNHandler(c *gin.Context) {
+ // 获取请求参数
+ var request struct {
+ ISBN string `json:"isbn" binding:"required"`
+ }
+
+ if err := c.ShouldBindJSON(&request); err != nil {
+ c.JSON(400, gin.H{
+ "error": "请求参数错误",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ isbn := strings.TrimSpace(request.ISBN)
+ if isbn == "" {
+ c.JSON(400, gin.H{
+ "error": "ISBN不能为空",
+ })
+ return
+ }
+
+ // 根据ISBN查询图书
+ book, err := svc.SearchBookByISBN(isbn)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "查询图书失败",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ if book == nil {
+ c.JSON(404, gin.H{
+ "error": "未找到该ISBN对应的图书",
+ })
+ return
+ }
+
+ // 检查书名是否包含套装关键字
+ isSuit := 0
+ if CheckBookSuit(book.BookName.Value) {
+ isSuit = 1
+ log.Printf("书名 '%s' 包含套装关键字,is_suit设置为1", book.BookName.Value)
+ } else {
+ log.Printf("书名 '%s' 不包含套装关键字,is_suit设置为0", book.BookName.Value)
+ }
+
+ // 更新ES中的is_suit字段
+ body := map[string]interface{}{
+ "script": map[string]interface{}{
+ "source": "ctx._source.is_suit = params.is_suit;",
+ "lang": "painless",
+ "params": map[string]interface{}{
+ "is_suit": isSuit,
+ },
+ },
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ "isbn": isbn,
+ },
+ },
+ }
+
+ payload, _ := json.Marshal(body)
+
+ res, err := svc.ES.Client.UpdateByQuery(
+ []string{ESIndex},
+ svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)),
+ svc.ES.Client.UpdateByQuery.WithRefresh(true),
+ svc.ES.Client.UpdateByQuery.WithConflicts("proceed"),
+ )
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "更新ES失败",
+ "details": err.Error(),
+ })
+ return
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ c.JSON(500, gin.H{
+ "error": "ES返回错误",
+ "details": res.String(),
+ })
+ return
+ }
+
+ // 解析更新结果
+ var parsed struct {
+ Total int `json:"total"`
+ Updated int `json:"updated"`
+ VersionConflicts int `json:"version_conflicts"`
+ }
+ if err := json.NewDecoder(res.Body).Decode(&parsed); err != nil {
+ c.JSON(500, gin.H{
+ "error": "解析更新结果失败",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ // 返回结果
+ c.JSON(200, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": gin.H{
+ "isbn": isbn,
+ "book_name": book.BookName.Value,
+ "is_suit": isSuit,
+ "updated": parsed.Updated,
+ "contains_suit_keyword": isSuit == 1,
+ },
+ })
+}
+
+// UpdateBookFieldsByISBNHandler 根据ISBN通用更新图书字段
+func (svc *ESSearchService) UpdateBookFieldsByISBNHandler(c *gin.Context) {
+ // 请求参数
+ var request struct {
+ ISBN string `json:"isbn" binding:"required"`
+ Data map[string]interface{} `json:"data" binding:"required"`
+ }
+
+ // 参数解析
+ if err := c.ShouldBindJSON(&request); err != nil {
+ c.JSON(400, gin.H{
+ "error": "请求参数错误",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ isbn := strings.TrimSpace(request.ISBN)
+ if isbn == "" {
+ c.JSON(400, gin.H{
+ "error": "ISBN不能为空",
+ })
+ return
+ }
+
+ if len(request.Data) == 0 {
+ c.JSON(400, gin.H{
+ "error": "至少提供一个要更新的字段",
+ })
+ return
+ }
+
+ // 先确认 ISBN 是否存在
+ book, err := svc.SearchBookByISBN(isbn)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "查询ES失败",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ if book == nil {
+ c.JSON(404, gin.H{
+ "error": "未找到该ISBN对应的图书",
+ })
+ return
+ }
+
+ // 调用 Service 层更新逻辑
+ updated, err := svc.UpdateBookFieldsByISBN(
+ c.Request.Context(),
+ isbn,
+ request.Data,
+ )
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "更新ES失败",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ // 返回结果
+ c.JSON(200, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": gin.H{
+ "isbn": isbn,
+ "updated": updated,
+ "fields_updated": len(request.Data),
+ "updated_fields": func() []string {
+ fields := make([]string, 0, len(request.Data))
+ for k := range request.Data {
+ fields = append(fields, k)
+ }
+ return fields
+ }(),
+ },
+ })
+}
+
+func (svc *ESSearchService) UpdateBookFieldsByISBN(
+ ctx context.Context,
+ isbn string,
+ data map[string]interface{},
+) (int, error) {
+
+ if isbn == "" {
+ return 0, fmt.Errorf("ISBN不能为空")
+ }
+ if len(data) == 0 {
+ return 0, fmt.Errorf("至少提供一个要更新的字段")
+ }
+
+ // 构建更新脚本
+ var scriptParts []string
+ params := make(map[string]interface{})
+
+ allowedFields := map[string]bool{
+ "book_name": true,
+ "book_pic": true,
+ "book_pic_s": true,
+ "book_pic_b": true,
+ "book_pic_w": true,
+ "author": true,
+ "category": true,
+ "publisher": true,
+ "publication_time": true,
+ "binding_layout": true,
+ "fix_price": true,
+ "content": true,
+ "is_suit": true,
+ "day_sale_7": true,
+ "day_sale_15": true,
+ "day_sale_30": true,
+ "day_sale_60": true,
+ "day_sale_90": true,
+ "day_sale_180": true,
+ "day_sale_365": true,
+ "this_year_sale": true,
+ "last_year_sale": true,
+ "total_sale": true,
+ "buy_counts": true,
+ "sell_counts": true,
+ "is_illegal": true,
+ "is_return": true,
+ "is_filter": true,
+ "update_time": true,
+ }
+
+ for field, value := range data {
+ if !allowedFields[field] {
+ continue
+ }
+ scriptParts = append(scriptParts,
+ fmt.Sprintf("ctx._source.%s = params.%s;", field, field))
+ params[field] = value
+ }
+
+ if len(scriptParts) == 0 {
+ return 0, fmt.Errorf("没有有效的字段可更新")
+ }
+
+ body := map[string]interface{}{
+ "script": map[string]interface{}{
+ "source": strings.Join(scriptParts, " "),
+ "lang": "painless",
+ "params": params,
+ },
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ "isbn": isbn,
+ },
+ },
+ }
+
+ payload, _ := json.Marshal(body)
+
+ res, err := svc.ES.Client.UpdateByQuery(
+ []string{ESIndex},
+ svc.ES.Client.UpdateByQuery.WithBody(bytes.NewReader(payload)),
+ svc.ES.Client.UpdateByQuery.WithRefresh(true),
+ svc.ES.Client.UpdateByQuery.WithConflicts("proceed"),
+ )
+ if err != nil {
+ return 0, err
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return 0, fmt.Errorf("ES返回错误: %s", res.String())
+ }
+
+ var parsed struct {
+ Updated int `json:"updated"`
+ }
+ _ = json.NewDecoder(res.Body).Decode(&parsed)
+
+ return parsed.Updated, nil
+}
+
+// AddBookToESHandler 根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新
+func (svc *ESSearchService) AddBookToESHandler(c *gin.Context) {
+ var req ESBook
+
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "参数解析错误",
+ "message": err.Error(),
+ })
+ return
+ }
+
+ if req.ISBN == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "ISBN不能为空",
+ })
+ return
+ }
+ // 打印接收参数
+ log.Printf("接收参数: %+v", req)
+
+ // ====== 外层:先查 ES 是否存在 ======
+ book, err := svc.SearchBookByISBN(req.ISBN)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "查询ES失败",
+ "msg": err.Error(),
+ })
+ return
+ }
+
+ // ====== 已存在直接处理 ======
+ if book != nil {
+ log.Printf("ES中已存在: %+v", book)
+
+ updateData := make(map[string]interface{})
+
+ if book.Publisher == "" && req.Publisher != "" {
+ updateData["publisher"] = req.Publisher
+ }
+ if book.PublicationTime == "" && req.PublicationTime != "" {
+ updateData["publication_time"] = req.PublicationTime
+ }
+ if book.BookPic.PddPath == "" && req.BookPic.PddPath != "" {
+ updateData["book_pic"] = map[string]interface{}{
+ "localPath": "",
+ "pddPath": req.BookPic.PddPath,
+ }
+ }
+ if book.BookPicS.PddResponse == "" && req.BookPicS.PddResponse != "" {
+ updateData["book_pic_s"] = map[string]interface{}{
+ "localPath": "",
+ "pddResponse": req.BookPicS.PddResponse,
+ }
+ }
+ if book.BookName.Value == "" && req.BookName.Value != "" {
+ updateData["book_name"] = req.BookName.Value
+ }
+ if book.Author == "" && req.Author != "" {
+ updateData["author"] = req.Author
+ }
+ // 打印传递的参数
+ log.Printf("更新数据: %+v", updateData)
+
+ if len(updateData) > 0 {
+ updateData["update_time"] = fmt.Sprintf("%d", time.Now().Unix())
+
+ _, err := svc.UpdateBookFieldsByISBN(
+ c.Request.Context(),
+ req.ISBN,
+ updateData,
+ )
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "补全ES字段失败",
+ "msg": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 重新查询最新数据
+ newBook, _ := svc.SearchBookByISBN(req.ISBN)
+
+ c.JSON(200, gin.H{
+ "data": newBook.ConvertToResponse(),
+ "source": "es",
+ })
+ return
+ }
+
+ // ====== 不存在,才真正新增 ======
+ newBook, err := svc.AddBookToES(c.Request.Context(), &req)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "data": newBook.ConvertToResponse(),
+ "source": "service",
+ })
+}
+
+// AddBookFullToESHandler 新的完整插入接口,支持所有字段,book_name强制为string类型
+func (svc *ESSearchService) AddBookFullToESHandler(c *gin.Context) {
+ var req AddBookFullRequest
+
+ // 参数校验
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "参数解析错误",
+ "message": err.Error(),
+ })
+ return
+ }
+
+ // 强制校验 book_name 为字符串类型
+ //if req.BookName == "" {
+ // c.JSON(http.StatusBadRequest, gin.H{
+ // "error": "book_name 不能为空",
+ // "message": "book_name 字段必须为非空字符串",
+ // })
+ // return
+ //}
+
+ // ISBN 必填校验
+ if req.ISBN == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "ISBN 不能为空",
+ "message": "ISBN 字段为必填项",
+ })
+ return
+ }
+
+ // 验证 BookPic 对象格式
+ if req.BookPic != nil {
+ if err := validateBookPicObj(req.BookPic, "book_pic"); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "book_pic 格式错误",
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 验证 BookPicS 对象格式
+ if req.BookPicS != nil {
+ if err := validateBookPicSObj(req.BookPicS, "book_pic_s"); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "book_pic_s 格式错误",
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 验证 BookPicW 对象格式
+ if req.BookPicW != nil {
+ if err := validateBookPicWObj(req.BookPicW, "book_pic_w"); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "book_pic_w 格式错误",
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 验证 BookPicObj 对象格式
+ if req.BookPicObj != nil {
+ if err := validateBookPicObj(req.BookPicObj, "book_pic_obj"); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "book_pic_obj 格式错误",
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 验证 BookPicObjS 对象格式
+ if req.BookPicObjS != nil {
+ if err := validateBookPicObjS(req.BookPicObjS, "book_pic_obj_s"); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "book_pic_obj_s 格式错误",
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 验证数值字段范围
+ if req.FixPrice < 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "fix_price 不能为负数",
+ "message": "fix_price 字段必须为非负数",
+ })
+ return
+ }
+
+ if req.IsSuit < 0 || req.IsSuit > 1 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "is_suit 值错误",
+ "message": "is_suit 字段必须为 0 或 1",
+ })
+ return
+ }
+
+ // 验证销量字段不能为负数
+ salesFields := []struct {
+ name string
+ value int
+ }{
+ {"day_sale_7", req.DaySale7},
+ {"day_sale_15", req.DaySale15},
+ {"day_sale_30", req.DaySale30},
+ {"day_sale_60", req.DaySale60},
+ {"day_sale_90", req.DaySale90},
+ {"day_sale_180", req.DaySale180},
+ {"day_sale_365", req.DaySale365},
+ {"this_year_sale", req.ThisYearSale},
+ {"last_year_sale", req.LastYearSale},
+ {"total_sale", req.TotalSale},
+ }
+
+ for _, field := range salesFields {
+ if field.value < 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": fmt.Sprintf("%s 不能为负数", field.name),
+ "message": fmt.Sprintf("%s 字段必须为非负数", field.name),
+ })
+ return
+ }
+ }
+
+ // 验证购买和售卖次数不能为负数
+ if req.BuyCounts < 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "buy_counts 不能为负数",
+ "message": "buy_counts 字段必须为非负数",
+ })
+ return
+ }
+
+ if req.SellCounts < 0 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "sell_counts 不能为负数",
+ "message": "sell_counts 字段必须为非负数",
+ })
+ return
+ }
+
+ // 验证字符串字段长度(防止过长导致存储问题)
+ if len(req.BookName) > 500 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "book_name 过长",
+ "message": "book_name 字段长度不能超过500个字符",
+ })
+ return
+ }
+
+ if len(req.Author) > 200 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "author 过长",
+ "message": "author 字段长度不能超过200个字符",
+ })
+ return
+ }
+
+ if len(req.Publisher) > 200 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "publisher 过长",
+ "message": "publisher 字段长度不能超过200个字符",
+ })
+ return
+ }
+
+ if len(req.ISBN) > 20 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "isbn 过长",
+ "message": "isbn 字段长度不能超过20个字符",
+ })
+ return
+ }
+
+ // 验证ISBN格式(简单验证)
+ if req.ISBN != "" {
+ // 移除连字符和空格后检查是否只包含数字
+ cleanISBN := strings.ReplaceAll(strings.ReplaceAll(req.ISBN, "-", ""), " ", "")
+ if len(cleanISBN) < 10 || len(cleanISBN) > 13 {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "isbn 格式错误",
+ "message": "isbn 格式不正确,应为10-13位数字",
+ })
+ return
+ }
+
+ // 检查是否只包含数字
+ for _, r := range cleanISBN {
+ if r < '0' || r > '9' {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "isbn 格式错误",
+ "message": "isbn 只能包含数字",
+ })
+ return
+ }
+ }
+ }
+
+ // 转换为 ESBook 结构体
+ esBook := &ESBook{
+ BookName: FlexibleString{Value: req.BookName}, // 强制使用字符串
+ ISBN: req.ISBN,
+ Author: req.Author,
+ Category: req.Category,
+ Publisher: req.Publisher,
+ PublicationTime: req.PublicationTime,
+ BindingLayout: req.BindingLayout,
+ FixPrice: Float64OrString(req.FixPrice),
+ Content: req.Content,
+ IsSuit: req.IsSuit,
+ DaySale7: req.DaySale7,
+ DaySale15: req.DaySale15,
+ DaySale30: req.DaySale30,
+ DaySale60: req.DaySale60,
+ DaySale90: req.DaySale90,
+ DaySale180: req.DaySale180,
+ DaySale365: req.DaySale365,
+ ThisYearSale: req.ThisYearSale,
+ LastYearSale: req.LastYearSale,
+ TotalSale: req.TotalSale,
+ BuyCounts: req.BuyCounts,
+ SellCounts: req.SellCounts,
+ BookPicObj: req.BookPicObj,
+ BookPicObjS: req.BookPicObjS,
+ IsIllegal: req.IsIllegal,
+ IsReturn: req.IsReturn,
+ IsFilter: req.IsFilter,
+ }
+
+ // 处理复杂图片字段(已经过验证,直接使用)
+ // BookPic
+ if req.BookPic != nil {
+ localPath := req.BookPic["localPath"].(string)
+ pddPath := req.BookPic["pddPath"].(string)
+ esBook.BookPic = BookPicObj{
+ LocalPath: localPath,
+ PddPath: pddPath,
+ }
+ } else {
+ esBook.BookPic = BookPicObj{LocalPath: "", PddPath: ""}
+ }
+
+ // BookPicS
+ if req.BookPicS != nil {
+ localPath := req.BookPicS["localPath"].(string)
+ pddResponse := req.BookPicS["pddResponse"].(string)
+ esBook.BookPicS = BookPicSObj{
+ LocalPath: localPath,
+ PddResponse: pddResponse,
+ }
+ } else {
+ esBook.BookPicS = BookPicSObj{LocalPath: "", PddResponse: ""}
+ }
+
+ // BookPicB
+ esBook.BookPicB = req.BookPicB
+
+ // BookPicW
+ if req.BookPicW != nil {
+ esBook.BookPicW = req.BookPicW
+ } else {
+ esBook.BookPicW = make(map[string]interface{})
+ }
+
+ // 调用 AddBookToES 方法插入数据
+ book, err := svc.AddBookToES(c.Request.Context(), esBook)
+ if err != nil {
+ c.JSON(500, gin.H{
+ "error": "插入ES失败",
+ "message": err.Error(),
+ })
+ return
+ }
+
+ // 转换为Java兼容的响应格式
+ responseData := book.ConvertToResponse()
+
+ c.JSON(http.StatusOK, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": responseData,
+ "source": "full_insert",
+ })
+}
+
+// AddBookToES 新增数据到es中 或者更新
+func (svc *ESSearchService) AddBookToES(ctx context.Context, req *ESBook) (*ESBook, error) {
+ // 打印入参
+ log.Printf("入参: %+v", req)
+ if req.ISBN == "" {
+ return nil, fmt.Errorf("ISBN不能为空")
+ }
+
+ now := time.Now().Unix()
+
+ // 解析 book_pic_new
+ bookPicObjS := map[string]interface{}{
+ "pddResponse": req.BookPicS.PddResponse,
+ }
+ // 获取销量数据
+ salesData, _ := tail.CheckSales([]string{req.ISBN})
+ log.Printf("销量数据: %+v", salesData)
+ var daySale7, daySale15, daySale30, daySale60, daySale90, daySale180, daySale365 int
+ var thisYearSale, lastYearSale, totalSale int
+
+ if salesData != nil && salesData.Data != nil {
+ if s, ok := salesData.Data[req.ISBN]; ok {
+ parse := func(str string) int {
+ if v, err := strconv.Atoi(str); err == nil {
+ return v
+ }
+ return 0
+ }
+ daySale7 = parse(s.DaySale7)
+ daySale15 = parse(s.DaySale15)
+ daySale30 = parse(s.DaySale30)
+ daySale60 = parse(s.DaySale60)
+ daySale90 = parse(s.DaySale90)
+ daySale180 = parse(s.DaySale180)
+ daySale365 = parse(s.DaySale365)
+ thisYearSale = parse(s.ThisYearSale)
+ lastYearSale = parse(s.LastYearSale)
+ totalSale = parse(s.Sale)
+ }
+ }
+
+ if totalSale == 0 && req.BuyCounts > 0 {
+ totalSale = int(req.BuyCounts)
+ }
+
+ // 套装判断
+ isSuit := 0
+ if CheckBookSuit(req.BookName.Value) {
+ isSuit = 1
+ }
+
+ // 生成新 ID 以便新增数据插入新ID
+ lastID, err := svc.GetLastID()
+ if err != nil {
+ return nil, fmt.Errorf("获取最后ID失败: %w", err)
+ }
+ newID := lastID + 1
+
+ newBook := ESBook{
+ ID: int64(newID),
+ BookName: FlexibleString{Value: req.BookName.Value},
+ BookPic: BookPicObj{
+ LocalPath: "",
+ PddPath: req.BookPic.PddPath,
+ },
+ BookPicS: BookPicSObj{
+ LocalPath: "",
+ PddResponse: req.BookPicS.PddResponse,
+ },
+ BookPicW: make(map[string]interface{}),
+ ISBN: req.ISBN,
+ Author: req.Author,
+ Publisher: req.Publisher,
+ PublicationTime: req.PublicationTime,
+ FixPrice: req.FixPrice,
+ IsSuit: isSuit,
+
+ DaySale7: daySale7,
+ DaySale15: daySale15,
+ DaySale30: daySale30,
+ DaySale60: daySale60,
+ DaySale90: daySale90,
+ DaySale180: daySale180,
+ DaySale365: daySale365,
+ ThisYearSale: thisYearSale,
+ LastYearSale: lastYearSale,
+ TotalSale: totalSale,
+
+ BuyCounts: req.BuyCounts,
+ SellCounts: req.SellCounts,
+ BookPicObjS: bookPicObjS,
+ UpdateTime: NumberOrString(fmt.Sprintf("%d", now)),
+ IsIllegal: 0,
+ IsReturn: 0,
+ IsFilter: "000000",
+ }
+ // 打印
+ log.Printf("生成新数据: %+v", newBook)
+
+ // 创建临时map用于序列化,避免FlexibleString的序列化问题
+ tempBook := map[string]interface{}{
+ "id": newBook.ID,
+ "book_name": newBook.BookName.Value, // 直接使用字符串值
+ "book_pic": newBook.BookPic,
+ "book_pic_s": newBook.BookPicS,
+ "book_pic_b": newBook.BookPicB,
+ "book_pic_w": newBook.BookPicW,
+ "isbn": newBook.ISBN,
+ "author": newBook.Author,
+ "category": newBook.Category,
+ "publisher": newBook.Publisher,
+ "publication_time": newBook.PublicationTime,
+ "binding_layout": newBook.BindingLayout,
+ "fix_price": newBook.FixPrice,
+ "content": newBook.Content,
+ "is_suit": newBook.IsSuit,
+ "day_sale_7": newBook.DaySale7,
+ "day_sale_15": newBook.DaySale15,
+ "day_sale_30": newBook.DaySale30,
+ "day_sale_60": newBook.DaySale60,
+ "day_sale_90": newBook.DaySale90,
+ "day_sale_180": newBook.DaySale180,
+ "day_sale_365": newBook.DaySale365,
+ "this_year_sale": newBook.ThisYearSale,
+ "last_year_sale": newBook.LastYearSale,
+ "total_sale": newBook.TotalSale,
+ "buy_counts": newBook.BuyCounts,
+ "sell_counts": newBook.SellCounts,
+ "book_pic_obj": newBook.BookPicObj,
+ "book_pic_obj_s": newBook.BookPicObjS,
+ "is_illegal": newBook.IsIllegal,
+ "is_return": newBook.IsReturn,
+ "is_filter": newBook.IsFilter,
+ "update_time": newBook.UpdateTime,
+ }
+
+ log.Printf("[AddBookToES] 准备序列化tempBook的book_name: %s", tempBook["book_name"])
+ jsonData, err := json.Marshal(tempBook)
+ if err != nil {
+ return nil, fmt.Errorf("序列化失败: %w", err)
+ }
+ log.Printf("[AddBookToES] 序列化结果: %s", string(jsonData))
+
+ // 写入 ES
+ esReq := esapi.IndexRequest{
+ Index: ESIndex,
+ DocumentID: req.ISBN,
+ Body: bytes.NewReader(jsonData),
+ Refresh: "true",
+ }
+ log.Printf("写入 ES 请求: %+v", esReq)
+
+ res, err := esReq.Do(ctx, svc.ES.Client.Transport)
+ //log.Printf("写入 ES 响应: %+v", res)
+ if err != nil {
+ return nil, fmt.Errorf("ES写入失败: %w", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return nil, fmt.Errorf("ES返回错误: %s", res.String())
+ }
+
+ return &newBook, nil
+}
+
+// GetLastID 查询最后一条文档的ID
+func (svc *ESSearchService) GetLastID() (int, error) {
+ log.Println("[GetLastID] 开始查询最新文档 ID")
+
+ query := `{
+ "size": 1,
+ "sort": [{"id": {"order": "desc"}}]
+ }`
+
+ res, err := svc.ES.Client.Search(
+ svc.ES.Client.Search.WithContext(context.Background()),
+ svc.ES.Client.Search.WithIndex(ESIndex),
+ svc.ES.Client.Search.WithBody(strings.NewReader(query)),
+ )
+ if err != nil {
+ log.Printf("[GetLastID] ES 查询失败: %v\n", err)
+ return 0, fmt.Errorf("ES 查询失败: %w", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ log.Printf("[GetLastID] ES 返回错误: %s\n", res.String())
+ return 0, fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+ // 打印日志
+ //log.Println("[GetLastID] ES 响应:", res.String())
+ //log.Println("[GetLastID] 响应内容:", res.Body)
+ // 定义结构体,ID 为数组
+ var result struct {
+ Hits struct {
+ Hits []struct {
+ Source struct {
+ ID int `json:"id"` // 改回单个 int
+ } `json:"_source"`
+ } `json:"hits"`
+ } `json:"hits"`
+ }
+
+ if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
+ log.Printf("[GetLastID] 解析 ES 返回 JSON 失败: %v\n", err)
+ return 0, fmt.Errorf("解析 ES 返回 JSON 失败: %w", err)
+ }
+
+ if len(result.Hits.Hits) == 0 {
+ log.Println("[GetLastID] 没有找到任何文档")
+ return 0, nil
+ }
+ lastID := result.Hits.Hits[0].Source.ID
+ log.Printf("[GetLastID] 查询到最新文档 ID: %d\n", lastID)
+
+ return lastID, nil
+}
+
+// DeleteBookHandler 删除图书
+func (svc *ESSearchService) DeleteBookHandler(c *gin.Context) {
+ isbn := c.Query("isbn")
+ if err := svc.DeleteBookByISBN(isbn); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
+}
+
+// DeleteBookByISBN 通过 ISBN 删除 ES 文档
+//
+// func (svc *ESSearchService) DeleteBookByISBN(isbn string) error {
+// isbn = strings.TrimSpace(isbn)
+// log.Printf("[DeleteBookByISBN] 开始删除 | ISBN=%s", isbn)
+//
+// if isbn == "" {
+// log.Printf("[DeleteBookByISBN] ISBN 为空,取消删除")
+// return fmt.Errorf("ISBN 不能为空")
+// }
+//
+// // 查询文档,确认存在
+// byISBN, err := svc.SearchBookByISBN(isbn)
+// if err != nil {
+// log.Printf("[DeleteBookByISBN] 查询 ISBN 出错: %v", err)
+// return err
+// }
+// if byISBN == nil {
+// log.Printf("[DeleteBookByISBN] 未找到 ISBN 对应的文档: %s", isbn)
+// return fmt.Errorf("未找到 ISBN 对应的文档: %s", isbn)
+// }
+//
+// log.Printf("[DeleteBookByISBN] 找到文档: %+v", byISBN)
+//
+// // 构建删除请求
+// req := esapi.DeleteRequest{
+// Index: ESIndex,
+// DocumentID: isbn, // 确认文档 ID 与 ISBN 一致,否则需要 DeleteByQuery
+// Refresh: "true",
+// }
+//
+// // 执行删除请求
+// res, err := req.Do(context.Background(), svc.ES.Client.Transport)
+// if err != nil {
+// log.Printf("[DeleteBookByISBN] 执行 ES 删除请求失败: %v", err)
+// return fmt.Errorf("执行 ES 删除请求失败: %v", err)
+// }
+// defer res.Body.Close()
+//
+// // 检查响应状态
+// if res.IsError() {
+// if res.StatusCode == 404 {
+// log.Printf("[DeleteBookByISBN] 文档不存在,已跳过删除 | ISBN=%s", isbn)
+// return nil
+// }
+// log.Printf("[DeleteBookByISBN] ES 返回错误: %s", res.String())
+// return fmt.Errorf("ES 返回错误: %s", res.String())
+// }
+//
+// // 解析响应结果
+// var result struct {
+// Result string `json:"result"`
+// Version int `json:"_version"`
+// }
+// if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
+// log.Printf("[DeleteBookByISBN] 解析 ES 删除响应失败: %v", err)
+// return fmt.Errorf("解析 ES 删除响应失败: %v", err)
+// }
+//
+// log.Printf("[DeleteBookByISBN] 成功删除 ES 文档 | ISBN=%s | result=%s | version=%d", isbn, result.Result, result.Version)
+// return nil
+// }
+func (svc *ESSearchService) DeleteBookByISBN(isbn string) error {
+ isbn = strings.TrimSpace(isbn)
+ log.Printf("[DeleteBookByISBN] 开始删除 | ISBN=%s", isbn)
+
+ if isbn == "" {
+ return fmt.Errorf("ISBN 不能为空")
+ }
+
+ //index := "books-from-mysql-v2"
+
+ query := fmt.Sprintf(`{
+ "query": {
+ "term": {
+ "isbn": "%s"
+ }
+ }
+ }`, isbn)
+
+ res, err := svc.ES.Client.DeleteByQuery(
+ []string{ESIndex},
+ strings.NewReader(query),
+ svc.ES.Client.DeleteByQuery.WithRefresh(true),
+ )
+ if err != nil {
+ return fmt.Errorf("执行 DeleteByQuery 失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ log.Printf("[DeleteBookByISBN] 成功删除 ISBN=%s 对应的文档", isbn)
+ return nil
+}
+
+// DeleteBookByID 通过 ID 删除 ES 文档
+func (svc *ESSearchService) DeleteBookByID(id string) error {
+ id = strings.TrimSpace(id)
+ log.Printf("[DeleteBookByID] 开始删除 | ID=%s", id)
+
+ if id == "" {
+ return fmt.Errorf("ID 不能为空")
+ }
+
+ query := fmt.Sprintf(`{
+ "query": {
+ "term": {
+ "id": "%s"
+ }
+ }
+ }`, id)
+
+ res, err := svc.ES.Client.DeleteByQuery(
+ []string{ESIndex},
+ strings.NewReader(query),
+ svc.ES.Client.DeleteByQuery.WithRefresh(true),
+ )
+ if err != nil {
+ return fmt.Errorf("执行 DeleteByQuery 失败: %v", err)
+ }
+ defer res.Body.Close()
+
+ if res.IsError() {
+ return fmt.Errorf("ES 返回错误: %s", res.String())
+ }
+
+ log.Printf("[DeleteBookByID] 成功删除 ID=%s 对应的文档", id)
+ return nil
+}
+
+// DeleteBookByIDHandler 根据ID删除图书的HTTP处理器
+func (svc *ESSearchService) DeleteBookByIDHandler(c *gin.Context) {
+ id := strings.TrimSpace(c.Query("id"))
+ if id == "" {
+ c.JSON(400, gin.H{
+ "error": "缺少 id 参数",
+ })
+ return
+ }
+
+ if err := svc.DeleteBookByID(id); err != nil {
+ c.JSON(500, gin.H{
+ "error": "删除失败",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(200, gin.H{
+ "code": 200,
+ "message": "success",
+ "data": gin.H{
+ "id": id,
+ "deleted": true,
+ },
+ })
+}
+
+// BatchAddBookRequest 批量插入请求结构体
+type BatchAddBookRequest struct {
+ Books []AddBookFullRequest `json:"books"` // 图书列表
+}
+
+// BatchInsertResultItem 单个图书插入结果
+type BatchInsertResultItem struct {
+ Index int `json:"index"` // 在批次中的索引
+ ISBN string `json:"isbn"` // 图书ISBN
+ Success bool `json:"success"` // 是否成功
+ Error string `json:"error"` // 错误信息(如果有)
+ Document string `json:"document"` // 文档ID(如果成功)
+}
+
+// BatchInsertResult 批量插入结果
+type BatchInsertResult struct {
+ TotalCount int `json:"total_count"` // 总数量
+ SuccessCount int `json:"success_count"` // 成功数量
+ FailedCount int `json:"failed_count"` // 失败数量
+ Results []BatchInsertResultItem `json:"results"` // 详细结果
+}
+
+// BatchAddBookToESHandler 批量插入图书到ES的HTTP处理器
+func (svc *ESSearchService) BatchAddBookToESHandler(c *gin.Context) {
+ var req BatchAddBookRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(400, gin.H{
+ "error": "请求参数格式错误",
+ "details": err.Error(),
+ })
+ return
+ }
+
+ // 验证至少提供一本书
+ if len(req.Books) == 0 {
+ c.JSON(400, gin.H{
+ "error": "至少提供一本要插入的图书",
+ })
+ return
+ }
+
+ // 限制批量插入数量,避免一次性处理过多
+ maxBatchSize := 100
+ if len(req.Books) > maxBatchSize {
+ c.JSON(400, gin.H{
+ "error": fmt.Sprintf("批量插入数量不能超过%d本", maxBatchSize),
+ })
+ return
+ }
+
+ result := BatchInsertResult{
+ TotalCount: len(req.Books),
+ SuccessCount: 0,
+ FailedCount: 0,
+ Results: make([]BatchInsertResultItem, 0, len(req.Books)),
+ }
+
+ // 逐个处理每本书
+ for i, book := range req.Books {
+ item := BatchInsertResultItem{
+ Index: i,
+ ISBN: book.ISBN,
+ Success: false,
+ }
+
+ // 验证必填字段
+ if book.BookName == "" {
+ item.Error = "book_name不能为空"
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+
+ if book.ISBN == "" {
+ item.Error = "ISBN不能为空"
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+
+ // 验证ISBN格式
+ if !validateISBNFormat(book.ISBN) {
+ item.Error = "ISBN格式不正确,应为10-13位数字"
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+
+ // 确保book_pic和book_pic_s存在且验证通过
+ if err := validateBookPicObj(book.BookPic, "book_pic"); err != nil {
+ item.Error = fmt.Sprintf("book_pic验证失败: %v", err)
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+
+ if err := validateBookPicSObj(book.BookPicS, "book_pic_s"); err != nil {
+ item.Error = fmt.Sprintf("book_pic_s验证失败: %v", err)
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+
+ if book.BookPicW != nil {
+ if err := validateBookPicWObj(book.BookPicW, "book_pic_w"); err != nil {
+ item.Error = fmt.Sprintf("book_pic_w验证失败: %v", err)
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+ }
+
+ // 转换为ESBook结构体
+ esBook := &ESBook{
+ BookName: FlexibleString{Value: book.BookName}, // 强制使用字符串
+ ISBN: book.ISBN,
+ Author: book.Author,
+ Category: book.Category,
+ Publisher: book.Publisher,
+ PublicationTime: book.PublicationTime,
+ BindingLayout: book.BindingLayout,
+ FixPrice: Float64OrString(book.FixPrice),
+ Content: book.Content,
+ IsSuit: book.IsSuit,
+ DaySale7: book.DaySale7,
+ DaySale15: book.DaySale15,
+ DaySale30: book.DaySale30,
+ DaySale60: book.DaySale60,
+ DaySale90: book.DaySale90,
+ DaySale180: book.DaySale180,
+ DaySale365: book.DaySale365,
+ ThisYearSale: book.ThisYearSale,
+ LastYearSale: book.LastYearSale,
+ TotalSale: book.TotalSale,
+ BuyCounts: book.BuyCounts,
+ SellCounts: book.SellCounts,
+ BookPicObj: book.BookPicObj,
+ BookPicObjS: book.BookPicObjS,
+ IsIllegal: book.IsIllegal,
+ IsReturn: book.IsReturn,
+ IsFilter: book.IsFilter,
+ }
+
+ // 处理复杂图片字段(已经过验证,直接使用)
+ // BookPic - 验证函数确保了要么为nil,要么包含正确的字段
+ if book.BookPic != nil {
+ localPath := book.BookPic["localPath"].(string)
+ pddPath := book.BookPic["pddPath"].(string)
+ esBook.BookPic = BookPicObj{
+ LocalPath: localPath,
+ PddPath: pddPath,
+ }
+ } else {
+ // 创建默认空对象
+ esBook.BookPic = BookPicObj{LocalPath: "", PddPath: ""}
+ }
+
+ // BookPicS - 验证函数确保了要么为nil,要么包含正确的字段
+ if book.BookPicS != nil {
+ localPath := book.BookPicS["localPath"].(string)
+ pddResponse := book.BookPicS["pddResponse"].(string)
+ esBook.BookPicS = BookPicSObj{
+ LocalPath: localPath,
+ PddResponse: pddResponse,
+ }
+ } else {
+ // 创建默认空对象
+ esBook.BookPicS = BookPicSObj{LocalPath: "", PddResponse: ""}
+ }
+
+ // BookPicB
+ esBook.BookPicB = book.BookPicB
+
+ // BookPicW
+ if book.BookPicW != nil {
+ esBook.BookPicW = book.BookPicW
+ } else {
+ esBook.BookPicW = make(map[string]interface{})
+ }
+
+ // 插入到ES
+ log.Printf("[BatchAddBookToESHandler] 准备插入书籍: ISBN=%s, BookName=%+v", book.ISBN, esBook.BookName)
+ addedBook, err := svc.AddBookToES(c.Request.Context(), esBook)
+ if err != nil {
+ item.Error = fmt.Sprintf("插入ES失败: %v", err)
+ result.Results = append(result.Results, item)
+ result.FailedCount++
+ continue
+ }
+
+ item.Success = true
+ item.Document = addedBook.ISBN // 使用ISBN作为文档标识
+ result.Results = append(result.Results, item)
+ result.SuccessCount++
+ }
+
+ // 根据整体结果返回相应的状态码
+ statusCode := 200
+ if result.SuccessCount == 0 {
+ statusCode = 500 // 全部失败
+ } else if result.FailedCount > 0 {
+ statusCode = 207 // 部分成功 (Multi-Status)
+ }
+
+ c.JSON(statusCode, gin.H{
+ "code": statusCode,
+ "message": "批量插入完成",
+ "data": result,
+ })
+}
+
+// validateISBNFormat 验证ISBN格式
+func validateISBNFormat(isbn string) bool {
+ if isbn == "" {
+ return false
+ }
+
+ // 移除连字符和空格后检查是否只包含数字
+ cleanISBN := strings.ReplaceAll(strings.ReplaceAll(isbn, "-", ""), " ", "")
+ if len(cleanISBN) < 10 || len(cleanISBN) > 13 {
+ return false
+ }
+
+ // 检查是否只包含数字
+ for _, r := range cleanISBN {
+ if r < '0' || r > '9' {
+ return false
+ }
+ }
+
+ return true
+}
+
+// CheckBookExistsRequest 检查书籍是否存在的请求结构体
+type CheckBookExistsRequest struct {
+ ISBN string `json:"isbn" binding:"required"` // 要查询的ISBN
+}
+
+// CheckBookExistsResponse 检查书籍是否存在的响应结构体
+type CheckBookExistsResponse struct {
+ Exists bool `json:"exists"` // 是否存在
+ ISBN string `json:"isbn"` // 查询的ISBN
+ BookID string `json:"book_id"` // 书籍ID(如果存在)
+ BookName string `json:"book_name"` // 书名(如果存在)
+ Message string `json:"message"` // 提示信息
+}
+
+// CheckBookExistsByISBN 根据ISBN检查书籍是否存在
+func (svc *ESSearchService) CheckBookExistsByISBN(isbn string) *CheckBookExistsResponse {
+ isbn = strings.TrimSpace(isbn)
+ log.Printf("[CheckBookExistsByISBN] 开始检查 | ISBN=%s", isbn)
+
+ if isbn == "" {
+ return &CheckBookExistsResponse{
+ Exists: false,
+ ISBN: "",
+ Message: "ISBN不能为空",
+ }
+ }
+
+ // 调用现有的SearchBookByISBN方法
+ book, err := svc.SearchBookByISBN(isbn)
+ if err != nil {
+ log.Printf("[CheckBookExistsByISBN] 查询失败: %v", err)
+ return &CheckBookExistsResponse{
+ Exists: false,
+ ISBN: isbn,
+ Message: fmt.Sprintf("查询失败: %v", err),
+ }
+ }
+
+ if book == nil {
+ log.Printf("[CheckBookExistsByISBN] 未找到书籍 | ISBN=%s", isbn)
+ return &CheckBookExistsResponse{
+ Exists: false,
+ ISBN: isbn,
+ Message: "未找到该ISBN对应的书籍",
+ }
+ }
+
+ log.Printf("[CheckBookExistsByISBN] 找到书籍 | ISBN=%s, BookName=%s", isbn, book.BookName.Value)
+ return &CheckBookExistsResponse{
+ Exists: true,
+ ISBN: isbn,
+ BookID: fmt.Sprintf("%d", book.ID),
+ BookName: book.BookName.Value,
+ Message: "书籍存在",
+ }
+}
+
+// CheckBookExistsByISBNHandler 根据ISBN检查书籍是否存在的HTTP处理器
+func (svc *ESSearchService) CheckBookExistsByISBNHandler(c *gin.Context) {
+ // 支持GET和POST两种方式
+ var req CheckBookExistsRequest
+ var _ error
+
+ // GET方式:从query参数获取
+ if c.Request.Method == "GET" {
+ isbn := strings.TrimSpace(c.Query("isbn"))
+ if isbn == "" {
+ c.JSON(400, gin.H{
+ "code": 400,
+ "error": "缺少ISBN参数",
+ "message": "请在请求中提供isbn参数,例如: ?isbn=9787020002207",
+ })
+ return
+ }
+ req.ISBN = isbn
+ } else {
+ // POST方式:从JSON body获取
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(400, gin.H{
+ "code": 400,
+ "error": "请求参数格式错误",
+ "message": err.Error(),
+ })
+ return
+ }
+ }
+
+ // 调用业务逻辑
+ result := svc.CheckBookExistsByISBN(req.ISBN)
+
+ // 根据结果返回不同的状态码
+ statusCode := 200
+ if !result.Exists && result.Message == "查询失败" {
+ statusCode = 500 // 查询出错
+ } else if !result.Exists {
+ statusCode = 404 // 未找到
+ }
+
+ c.JSON(statusCode, gin.H{
+ "code": statusCode,
+ "success": result.Exists,
+ "data": result,
+ })
+}
diff --git a/es_dll/.idea/es_dll.iml b/es_dll/.idea/es_dll.iml
new file mode 100644
index 0000000..c956989
--- /dev/null
+++ b/es_dll/.idea/es_dll.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/es_dll/.idea/modules.xml b/es_dll/.idea/modules.xml
new file mode 100644
index 0000000..e43d4f1
--- /dev/null
+++ b/es_dll/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/es_dll/.idea/vcs.xml b/es_dll/.idea/vcs.xml
new file mode 100644
index 0000000..6c0b863
--- /dev/null
+++ b/es_dll/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/es_dll/.idea/workspace.xml b/es_dll/.idea/workspace.xml
new file mode 100644
index 0000000..dcf7cd5
--- /dev/null
+++ b/es_dll/.idea/workspace.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1770725057358
+
+
+ 1770725057358
+
+
+
+
+
+
+
+ true
+
+
\ No newline at end of file
diff --git a/es_dll/es.dll b/es_dll/es.dll
new file mode 100644
index 0000000..4e41989
Binary files /dev/null and b/es_dll/es.dll differ
diff --git a/es_dll/es.md b/es_dll/es.md
new file mode 100644
index 0000000..8702889
--- /dev/null
+++ b/es_dll/es.md
@@ -0,0 +1,346 @@
+# es.dll 使用教程
+## 1.创建DLL工具实例
+### 加载DLL文件~~~~
+```gotemplate
+// EsDLL Elasticsearch工具DLL结构
+type esDLL struct {
+ dll *syscall.DLL
+ listAllIndices *syscall.Proc // 查询所有索引
+ getIndicesInfo *syscall.Proc // 获取所有索引的详细信息
+ getIndexDetail *syscall.Proc // 获取单个索引的详细信息
+ createIndex *syscall.Proc // 创建索引
+ deleteIndex *syscall.Proc // 删除索引
+ getDocumentCount *syscall.Proc // 获取索引文档数量
+ createDocument *syscall.Proc // 创建文档
+ getDocument *syscall.Proc // 根据ID获取文档
+ updateDocument *syscall.Proc // 更新文档
+ deleteDocument *syscall.Proc // 删除文档
+ searchDocuments *syscall.Proc // 搜索文档
+ freeCString *syscall.Proc // 释放C字符串
+}
+
+// 初始化esDLL
+func InitEsDLL() (*esDLL, error) {
+ dllPath := filepath.Join("dll", "es.dll")
+ if _, err := os.Stat(dllPath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("es DLL 不存在: %s", dllPath)
+ }
+ if dll, err := syscall.LoadDLL(dllPath); err != nil {
+ return nil, fmt.Errorf("加载es DLL 失败: %s", err)
+ } else {
+ return &esDLL{
+ dll: dll,
+ listAllIndices: dll.MustFindProc("ListAllIndices"),
+ getIndicesInfo: dll.MustFindProc("GetIndicesInfo"),
+ getIndexDetail: dll.MustFindProc("GetIndexDetail"),
+ createIndex: dll.MustFindProc("CreateIndex"),
+ deleteIndex: dll.MustFindProc("DeleteIndex"),
+ getDocumentCount: dll.MustFindProc("GetDocumentCount"),
+ createDocument: dll.MustFindProc("CreateDocument"),
+ getDocument: dll.MustFindProc("GetDocument"),
+ updateDocument: dll.MustFindProc("UpdateDocument"),
+ deleteDocument: dll.MustFindProc("DeleteDocument"),
+ searchDocuments: dll.MustFindProc("SearchDocuments"),
+ freeCString: dll.MustFindProc("FreeCString"),
+ }, nil
+ }
+}
+
+dll, err := InitEsDLL()
+```
+
+### 获取C字符串
+```gotemplate
+// cStr 获取C字符串
+func (m *esDLL) cStr(p uintptr) string {
+ if p == 0 {
+ return ""
+ }
+ b := []byte{}
+ for i := uintptr(0); ; i++ {
+ c := *(*byte)(unsafe.Pointer(p + i))
+ if c == 0 {
+ break
+ }
+ b = append(b, c)
+ }
+ s := string(b)
+ if m.freeCString != nil {
+ m.freeCString.Call(p)
+ }
+ return s
+}
+```
+
+## 2. 使用dll函数示例
+```gotemplate
+// 查询所有索引
+func (m *esDLL) ListAllIndices() (string, error) {
+ proc, err := m.dll.FindProc("ListAllIndices")
+ if err != nil {
+ return "", fmt.Errorf("找不到函数 ListAllIndices: %v", err)
+ }
+ resultPtr, _, _ := proc.Call()
+ result := m.cStr(resultPtr)
+ return result, nil
+}
+```
+
+# 接口详情
+## 1.查询所有索引--ListAllIndices
+### 请求信息
+```gotemplate
+dll.ListAllIndices()
+```
+### 请求参数
+无
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": ["index1", "index2", "index3"]
+}
+```
+
+## 2. 获取所有索引的详细信息--GetIndicesInfo
+### 请求信息
+```gotemplate
+dll.GetIndicesInfo()
+```
+### 请求参数
+无
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": [
+ {
+ "index": "index1",
+ "health": "green",
+ "status": "open",
+ "uuid": "abc123",
+ "pri": "1",
+ "rep": "1",
+ "docs.count": "100",
+ "docs.deleted": "0",
+ "store.size": "1.2kb",
+ "pri.store.size": "600b"
+ }
+ ]
+}
+```
+
+## 3. 获取单个索引的详细信息--GetIndexDetail
+### 请求信息
+```gotemplate
+dll.GetIndexDetail(indexName)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|----------|
+| indexName | string | 是 | 索引名称 |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": {
+ "index1": {
+ "aliases": {},
+ "mappings": {},
+ "settings": {
+ "index": {
+ "creation_date": "1234567890000",
+ "number_of_shards": "1",
+ "number_of_replicas": "1",
+ "uuid": "abc123",
+ "version": {
+ "created": "7080099"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+## 4. 创建索引--CreateIndex
+### 请求信息
+```gotemplate
+dll.CreateIndex(indexName, mapping)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+| mapping | string | 是 | 索引映射的JSON字符串 |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": {
+
+ }
+}
+```
+
+## 5. 删除索引--DeleteIndex
+### 请求信息
+```gotemplate
+dll.DeleteIndex(indexName)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": ""
+}
+```
+
+## 6. 获取索引文档数量--GetDocumentCount
+### 请求信息
+```gotemplate
+dll.GetDocumentCount(indexName)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": 100
+}
+```
+
+## 7. 创建文档--CreateDocument
+### 请求信息
+```gotemplate
+dll.CreateDocument(indexName, id, doc)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+| id | string | 是 | 文档ID |
+| doc | string | 是 | 文档内容的JSON字符串 |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": ""
+}
+```
+
+## 8. 根据ID获取文档--GetDocument
+### 请求信息
+```gotemplate
+dll.GetDocument(indexName, id)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+| id | string | 是 | 文档ID |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": {
+ "_id": "1",
+ "_index": "index1",
+ "_source": {
+ "title": "示例文档",
+ "content": "这是文档内容"
+ }
+ }
+}
+```
+
+## 9. 更新文档--UpdateDocument
+### 请求信息
+```gotemplate
+dll.UpdateDocument(indexName, id, updateData)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+| id | string | 是 | 文档ID |
+| updateData | string | 是 | 更新数据的JSON字符串 |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": ""
+}
+```
+
+## 10. 删除文档--DeleteDocument
+### 请求信息
+```gotemplate
+dll.DeleteDocument(indexName, id)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|---------------|
+| indexName | string | 是 | 索引名称 |
+| id | string | 是 | 文档ID |
+### 响应示例
+```json
+{
+ "success": true,
+ "message": ""
+}
+```
+
+## 11. 搜索文档--SearchDocuments
+### 请求信息
+```gotemplate
+dll.SearchDocuments(indexName, query)
+```
+### 请求参数
+| 参数名 | 类型 | 必填 | 说明 |
+|--|--|--|--------------|
+| indexName | string | 是 | 索引名称 |
+| query | string | 是 | 查询条件的JSON字符串|
+#### query示例
+```gotemplate
+query := map[string]interface{}{
+ "query": map[string]interface{}{
+ "term": map[string]interface{}{
+ fieldName: fieldValue,
+ },
+ },
+ "size": size,
+}
+```
+### 响应示例
+```json
+{
+ "success": true,
+ "message": "",
+ "data": [
+ {
+ "_id": "1",
+ "title": "示例文档",
+ "content": "这是文档内容"
+ },
+ {
+ "_id": "2",
+ "title": "另一个文档",
+ "content": "更多内容"
+ }
+ ]
+}
+```
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..35fe230
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,57 @@
+module centerBook
+
+go 1.24
+
+require (
+ github.com/elastic/go-elasticsearch/v8 v8.19.0
+ github.com/gin-contrib/cors v1.7.6
+ github.com/gin-gonic/gin v1.10.1
+ github.com/go-redis/redis/v8 v8.11.5
+ github.com/go-sql-driver/mysql v1.9.3
+ github.com/gorilla/websocket v1.5.3
+ github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/xuri/excelize/v2 v2.9.1
+)
+
+require (
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/bytedance/sonic v1.13.3 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
+ github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/elastic/elastic-transport-go/v8 v8.7.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.26.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/richardlehane/mscfb v1.0.4 // indirect
+ github.com/richardlehane/msoleps v1.0.4 // indirect
+ github.com/tiendc/go-deepcopy v1.6.0 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ github.com/xuri/efp v0.0.1 // indirect
+ github.com/xuri/nfp v0.0.1 // indirect
+ go.opentelemetry.io/otel v1.28.0 // indirect
+ go.opentelemetry.io/otel/metric v1.28.0 // indirect
+ go.opentelemetry.io/otel/trace v1.28.0 // indirect
+ golang.org/x/arch v0.18.0 // indirect
+ golang.org/x/crypto v0.39.0 // indirect
+ golang.org/x/net v0.41.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.26.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..dc8588d
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,150 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
+github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
+github.com/elastic/elastic-transport-go/v8 v8.7.0 h1:OgTneVuXP2uip4BA658Xi6Hfw+PeIOod2rY3GVMGoVE=
+github.com/elastic/elastic-transport-go/v8 v8.7.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
+github.com/elastic/go-elasticsearch/v8 v8.19.0 h1:VmfBLNRORY7RZL+9hTxBD97ehl9H8Nxf2QigDh6HuMU=
+github.com/elastic/go-elasticsearch/v8 v8.19.0/go.mod h1:F3j9e+BubmKvzvLjNui/1++nJuJxbkhHefbaT0kFKGY=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
+github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
+github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
+github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
+github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
+github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
+github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
+github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
+github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
+github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Qw=
+github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
+github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
+github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
+go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
+go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
+go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
+go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
+go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
+golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
+golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
+golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/health_api.go b/health_api.go
new file mode 100644
index 0000000..1cba748
--- /dev/null
+++ b/health_api.go
@@ -0,0 +1,393 @@
+package main
+
+import (
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+// SQLHealthController SQL健康监控控制器
+type SQLHealthController struct{}
+
+// NewSQLHealthController 创建SQL健康监控控制器
+func NewSQLHealthController() *SQLHealthController {
+ return &SQLHealthController{}
+}
+
+// GetSQLStats 获取SQL执行统计信息
+func (shc *SQLHealthController) GetSQLStats(c *gin.Context) {
+ if globalSQLMonitor == nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "SQL监控器未初始化",
+ })
+ return
+ }
+
+ stats := globalSQLMonitor.GetStats()
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": stats,
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetRecentSQLRecords 获取最近的SQL执行记录
+func (shc *SQLHealthController) GetRecentSQLRecords(c *gin.Context) {
+ if globalSQLMonitor == nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "SQL监控器未初始化",
+ })
+ return
+ }
+
+ // 获取limit参数,默认50条
+ limitStr := c.DefaultQuery("limit", "50")
+ limit, err := strconv.Atoi(limitStr)
+ if err != nil || limit <= 0 {
+ limit = 50
+ }
+ if limit > 500 {
+ limit = 500 // 最大限制500条
+ }
+
+ records := globalSQLMonitor.GetRecentRecords(limit)
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": gin.H{
+ "records": records,
+ "count": len(records),
+ "limit": limit,
+ },
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetSlowQueries 获取慢查询
+func (shc *SQLHealthController) GetSlowQueries(c *gin.Context) {
+ if globalSQLMonitor == nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "SQL监控器未初始化",
+ })
+ return
+ }
+
+ // 获取阈值参数,默认1000ms
+ thresholdStr := c.DefaultQuery("threshold", "1000")
+ threshold, err := strconv.ParseInt(thresholdStr, 10, 64)
+ if err != nil || threshold <= 0 {
+ threshold = 1000
+ }
+
+ slowQueries := globalSQLMonitor.GetSlowQueries(threshold)
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": gin.H{
+ "slow_queries": slowQueries,
+ "count": len(slowQueries),
+ "threshold_ms": threshold,
+ },
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetFailedQueries 获取失败的查询
+func (shc *SQLHealthController) GetFailedQueries(c *gin.Context) {
+ if globalSQLMonitor == nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "SQL监控器未初始化",
+ })
+ return
+ }
+
+ failedQueries := globalSQLMonitor.GetFailedQueries()
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "data": gin.H{
+ "failed_queries": failedQueries,
+ "count": len(failedQueries),
+ },
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// ClearSQLRecords 清空SQL记录
+func (shc *SQLHealthController) ClearSQLRecords(c *gin.Context) {
+ if globalSQLMonitor == nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "error": "SQL监控器未初始化",
+ })
+ return
+ }
+
+ globalSQLMonitor.ClearRecords()
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "success",
+ "message": "SQL记录已清空",
+ "timestamp": time.Now().Format("2006-01-02 15:04:05"),
+ })
+}
+
+// GetSQLHealthDashboard 获取SQL健康监控仪表板
+func (shc *SQLHealthController) GetSQLHealthDashboard(c *gin.Context) {
+ dashboardHTML := `
+
+
+
+
+
+ SQL健康监控仪表板
+
+
+
+
+
SQL健康监控仪表板
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+
+ c.Header("Content-Type", "text/html; charset=utf-8")
+ c.String(http.StatusOK, dashboardHTML)
+}
diff --git a/image/image.go b/image/image.go
new file mode 100644
index 0000000..1eb7350
--- /dev/null
+++ b/image/image.go
@@ -0,0 +1,139 @@
+package image
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "os"
+ "path/filepath"
+)
+
+// PddUploadResult 用于解析 PDD 上传返回的 result JSON
+type PddUploadResult struct {
+ URL string `json:"url"`
+}
+
+// UploadResponse 用于解析上传接口完整响应
+type UploadResponse struct {
+ Data struct {
+ PddUpload struct {
+ Result string `json:"result"`
+ Success bool `json:"success"`
+ } `json:"pddUpload"`
+ } `json:"data"`
+ Success bool `json:"success"`
+ Message string `json:"message"`
+}
+
+// DownloadFile 下载图片到临时文件
+func DownloadFile(url string) (string, error) {
+ resp, err := http.Get(url)
+ if err != nil {
+ return "", fmt.Errorf("下载文件失败: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("下载失败, 状态码: %d", resp.StatusCode)
+ }
+
+ tmpFile, err := os.CreateTemp("", "book_image_*.png")
+ if err != nil {
+ return "", fmt.Errorf("创建临时文件失败: %w", err)
+ }
+ defer tmpFile.Close()
+
+ _, err = io.Copy(tmpFile, resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("写入临时文件失败: %w", err)
+ }
+
+ return tmpFile.Name(), nil
+}
+
+// UploadBookImage 上传图片并返回 PDD URL
+func UploadBookImage(filePath, isbn, isKW, goodsName, autoUpload string) (string, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return "", fmt.Errorf("打开文件失败: %w", err)
+ }
+ defer file.Close()
+
+ var body bytes.Buffer
+ writer := multipart.NewWriter(&body)
+
+ part, err := writer.CreateFormFile("filepath", filepath.Base(file.Name()))
+ if err != nil {
+ return "", fmt.Errorf("创建文件字段失败: %w", err)
+ }
+ if _, err := io.Copy(part, file); err != nil {
+ return "", fmt.Errorf("复制文件内容失败: %w", err)
+ }
+
+ fields := map[string]string{
+ "isbn": isbn,
+ "isKW": isKW,
+ "goodsName": goodsName,
+ "autoUpload": autoUpload,
+ }
+
+ for k, v := range fields {
+ if err := writer.WriteField(k, v); err != nil {
+ return "", fmt.Errorf("写入字段 %s 失败: %w", k, err)
+ }
+ }
+
+ if err := writer.Close(); err != nil {
+ return "", fmt.Errorf("关闭 writer 失败: %w", err)
+ }
+
+ req, err := http.NewRequest("POST", "http://103.236.81.185:9000/processImage", &body)
+ if err != nil {
+ return "", fmt.Errorf("创建请求失败: %w", err)
+ }
+
+ req.Header.Set("Content-Type", writer.FormDataContentType())
+
+ client := &http.Client{}
+ resp, err := client.Do(req)
+ if err != nil {
+ return "", fmt.Errorf("发送请求失败: %w", err)
+ }
+ defer resp.Body.Close()
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("读取响应失败: %w", err)
+ }
+
+ var uploadResp UploadResponse
+ if err := json.Unmarshal(respBody, &uploadResp); err != nil {
+ return "", fmt.Errorf("解析响应失败: %w", err)
+ }
+
+ if !uploadResp.Data.PddUpload.Success {
+ return "", fmt.Errorf("上传失败: %s", uploadResp.Message)
+ }
+
+ // 解析 result 字段中的 JSON
+ var result PddUploadResult
+ if err := json.Unmarshal([]byte(uploadResp.Data.PddUpload.Result), &result); err != nil {
+ return "", fmt.Errorf("解析 PDD result 失败: %w", err)
+ }
+
+ return result.URL, nil
+}
+
+// DownloadAndUploadBookImage 下载图片并上传,返回 PDD URL
+func DownloadAndUploadBookImage(imageURL, isbn, isKW, goodsName, autoUpload string) (string, error) {
+ tmpFile, err := DownloadFile(imageURL)
+ if err != nil {
+ return "", err
+ }
+ defer os.Remove(tmpFile) // 使用完删除临时文件
+
+ return UploadBookImage(tmpFile, isbn, isKW, goodsName, autoUpload)
+}
diff --git a/ip_log.go b/ip_log.go
new file mode 100644
index 0000000..8f42e1f
--- /dev/null
+++ b/ip_log.go
@@ -0,0 +1,79 @@
+package main
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+)
+
+var (
+ ipLogFilePath string
+ ipLogMu sync.Mutex
+)
+
+// EnsureIPLogPathInitialized 初始化IP日志文件路径。
+// 作用:根据运行系统设置默认存储位置,并确保目录存在,避免写入失败。
+// 优先读取环境变量 IP_LOG_FILE(例如:/www/wwwroot/centerBook/ip.txt)。
+// Windows 默认:C:\Users\www\centerBook\ip.txt;Linux 默认:/www/wwwroot/centerBook/ip.txt。
+func EnsureIPLogPathInitialized() {
+ if ipLogFilePath != "" {
+ return
+ }
+ if env := os.Getenv("IP_LOG_FILE"); env != "" {
+ ipLogFilePath = env
+ } else {
+ if runtime.GOOS == "windows" {
+ ipLogFilePath = `C:\Users\www\centerBook\ip.txt`
+ } else {
+ ipLogFilePath = "/www/wwwroot/centerBook/ip.txt"
+ }
+ }
+ dir := filepath.Dir(ipLogFilePath)
+ _ = os.MkdirAll(dir, 0755)
+}
+
+// LogLargePerPage 记录超大 per_page 请求的IP信息到文本文件。
+// 触发条件:当 per_page 被裁剪(原始值大于上限)时调用。
+// 记录内容:时间、IP、方法、原始per_page、裁剪后per_page、路径、UA。
+func LogLargePerPage(c *gin.Context, originalPerPage, sanitizedPerPage int) {
+ EnsureIPLogPathInitialized()
+ line := fmt.Sprintf(
+ "%s | ip=%s | method=%s | per_page=%d->%d | path=%s | ua=%s\n",
+ time.Now().Format("2006-01-02 15:04:05"),
+ c.ClientIP(),
+ c.Request.Method,
+ originalPerPage,
+ sanitizedPerPage,
+ c.Request.RequestURI,
+ strings.ReplaceAll(c.GetHeader("User-Agent"), "\n", " "),
+ )
+ ipLogMu.Lock()
+ defer ipLogMu.Unlock()
+ f, err := os.OpenFile(ipLogFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+ if err != nil {
+ // 静默失败,避免影响主流程
+ return
+ }
+ defer f.Close()
+ _, _ = f.WriteString(line)
+}
+
+// IPLogHandler 返回 ip.txt 的内容(text/plain)。
+// 用途:通过路由暴露 ip.txt,便于反向代理将 https://book.center.buzhiyushu.cn/ip.txt 映射到此接口。
+func IPLogHandler() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ EnsureIPLogPathInitialized()
+ data, err := os.ReadFile(ipLogFilePath)
+ if err != nil {
+ c.String(200, "")
+ return
+ }
+ c.Header("Content-Type", "text/plain; charset=utf-8")
+ c.String(200, string(data))
+ }
+}
diff --git a/kongfz/kongfz.go b/kongfz/kongfz.go
new file mode 100644
index 0000000..de9df61
--- /dev/null
+++ b/kongfz/kongfz.go
@@ -0,0 +1,109 @@
+package kongfz
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+var TailTokens = []string{
+ "28fe4cf37dd6b2fa74f349236eb99c5c3914ebe3",
+ "4014640556b5b7c2181db662dea8775eb2c2469e",
+ "44c59815ae2c21935a88887b77cc89f5e3bc1fc3",
+ "69a0f30520e40fe617070d0688930366f544727f",
+ "1967ba84f30723c259b0dac2bd5734e025316ef8",
+ "39fade4d7167d94cef519d873362af1949990fde",
+ "cdc7d73a44d0ef8ca84b6618dde283ebca694892",
+ "8e93849cff58c83cca7227a6dc99abb56ac3d33f",
+ "1fd05fc23e8403431b724d816a115b579c77d515",
+ "b20b7d9728ceb5a4006104ef12d164e9438994e6",
+ "a97ab57a40df46de2bf850ceb854e8bab282cd33",
+}
+
+// BookResponse 孔网数据返回格式
+type BookResponse struct {
+ Data struct {
+ ISBN string `json:"isbn"`
+ Title string `json:"title"`
+ Author string `json:"author"`
+ Publisher string `json:"publisher"`
+ PublicationTime int64 `json:"publication_time"`
+ Price float64 `json:"price"`
+ BookName string `json:"book_name"`
+ FixPrice string `json:"fix_price"`
+ BindingLayout string `json:"binding_layout"`
+ BookPic string `json:"book_pic"`
+ BookPicS string `json:"book_pic_s"`
+ } `json:"data"`
+ Success bool `json:"success"`
+ Error string `json:"error"` // <- 新增
+}
+
+func GetBookImageByISBN(isbn, proxyType, username, password string) (*BookResponse, error) {
+ baseURL := "http://103.236.81.185:8989/api/outGetImageByIsbn"
+ client := &http.Client{Timeout: 10 * time.Second}
+
+ for _, token := range TailTokens {
+ params := url.Values{}
+ params.Set("isbn", isbn)
+ params.Set("token", token)
+ params.Set("proxyType", proxyType)
+ params.Set("username", username)
+ params.Set("password", password)
+
+ reqURL := baseURL + "?" + params.Encode()
+ log.Println("[GetBookImageByISBN] 请求参数:", reqURL)
+
+ req, err := http.NewRequest("GET", reqURL, nil)
+ if err != nil {
+ log.Println("[GetBookImageByISBN] 构建请求失败:", err)
+ continue
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ log.Println("[GetBookImageByISBN] 请求失败:", err)
+ continue
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ resp.Body.Close() // 立即关闭
+ if err != nil {
+ log.Println("[GetBookImageByISBN] 读取响应失败:", err)
+ continue
+ }
+
+ log.Println("[GetBookImageByISBN] 响应:", string(body))
+
+ var res BookResponse
+ if err := json.Unmarshal(body, &res); err != nil {
+ log.Println("[GetBookImageByISBN] 解析 JSON 失败:", err)
+ continue
+ }
+
+ log.Printf("[GetBookImageByISBN] 解析后的 Success=%v, Error=%s", res.Success, res.Error)
+
+ if res.Success {
+ log.Println("[GetBookImageByISBN] 成功返回 BookResponse")
+ return &res, nil
+ } else if strings.Contains(res.Error, "请登录后再进行访问") {
+ log.Println("[GetBookImageByISBN] Token 需要登录,尝试下一个 token")
+ continue
+ } else {
+ log.Println("[GetBookImageByISBN] 其他错误,直接返回")
+ return &res, errors.New(res.Error)
+ }
+ }
+
+ return nil, errors.New("所有 token 均失效或接口不可用")
+}
+
+// 简单包含判断
+func contains(s, substr string) bool {
+ return strings.Contains(s, substr)
+}
diff --git a/linux/goCenterBook b/linux/goCenterBook
new file mode 100644
index 0000000..7d2b256
Binary files /dev/null and b/linux/goCenterBook differ
diff --git a/linux/goCenterBook_linux b/linux/goCenterBook_linux
new file mode 100644
index 0000000..d331ef5
Binary files /dev/null and b/linux/goCenterBook_linux differ
diff --git a/linux/linux_linux b/linux/linux_linux
new file mode 100644
index 0000000..916fcd9
Binary files /dev/null and b/linux/linux_linux differ
diff --git a/logging_middleware.go b/logging_middleware.go
new file mode 100644
index 0000000..a4a62d4
--- /dev/null
+++ b/logging_middleware.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "github.com/gin-gonic/gin"
+ "log"
+ "time"
+)
+
+// RequestAuditLogger 请求审计中间件。
+// 作用:为每个进入的请求记录方法、原始URI、匹配路由(endpoint)、状态码、客户端IP、UA、耗时等关键信息,便于线上定位问题。
+// 使用:在创建路由后通过 r.Use(RequestAuditLogger()) 注册。
+func RequestAuditLogger() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ start := time.Now()
+ c.Next()
+
+ endpoint := c.FullPath() // Gin 匹配到的路由模板;未匹配则为空字符串
+ if endpoint == "" {
+ endpoint = ""
+ }
+
+ log.Printf(
+ "RequestAudit | %s %s | endpoint=%s | status=%d | ip=%s | ua=%s | cost=%v",
+ c.Request.Method,
+ c.Request.RequestURI,
+ endpoint,
+ c.Writer.Status(),
+ c.ClientIP(),
+ c.GetHeader("User-Agent"),
+ time.Since(start),
+ )
+ }
+}
+
+// NotFoundHandler 处理未匹配的路由。
+// 作用:对 404 的请求进行详细记录,并返回结构化的提示信息,方便快速定位来源与路径。
+// 使用:在路由注册完成后通过 r.NoRoute(NotFoundHandler) 注册。
+func NotFoundHandler(c *gin.Context) {
+ log.Printf(
+ "NoRoute 404 | %s %s | ip=%s | ua=%s | referer=%s",
+ c.Request.Method,
+ c.Request.RequestURI,
+ c.ClientIP(),
+ c.GetHeader("User-Agent"),
+ c.GetHeader("Referer"),
+ )
+ c.JSON(404, gin.H{
+ "code": 404,
+ "msg": "接口不存在",
+ "method": c.Request.Method,
+ "path": c.Request.RequestURI,
+ "client_ip": c.ClientIP(),
+ })
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..200e6a9
--- /dev/null
+++ b/main.go
@@ -0,0 +1,5662 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5"
+ "database/sql"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "net/url"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "reflect"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/gin-contrib/cors"
+ "github.com/go-redis/redis/v8"
+
+ "centerBook/es"
+
+ "github.com/gin-gonic/gin"
+ _ "github.com/go-sql-driver/mysql"
+ "github.com/gorilla/websocket"
+ "github.com/patrickmn/go-cache"
+ "github.com/xuri/excelize/v2"
+)
+
+// BookCenterController 图书中心控制器
+type BookCenterController struct {
+ //db *sql.DB
+ db *sql.DB // 原有数据库连接
+ cache *cache.Cache
+ redis *redis.Client // Redis客户端(使用go-redis库)
+ zhishuDB *sql.DB
+}
+
+// WebSocket连接升级器配置
+var upgrader = websocket.Upgrader{
+ ReadBufferSize: 1024,
+ WriteBufferSize: 1024,
+ CheckOrigin: func(r *http.Request) bool {
+ return true // 允许所有来源,生产环境应限制特定域名
+ },
+}
+
+// WebSocketManager 管理所有WebSocket连接
+type WebSocketManager struct {
+ clients map[*websocket.Conn]bool // 活跃客户端连接
+ broadcast chan []byte // 广播消息通道
+ mu sync.Mutex // 保护clients的互斥锁
+}
+
+const (
+ detailPagesUrl = "http://175.27.224.66:9999"
+ //detailPagesUrl = "http://10.206.0.10:8091" // 详情页面URL
+ bookBaseRedisurl = "36.212.1.63:6379" // 图书基础信息Redis
+ //bookBaseRedisurl = "10.206.0.17:6666"
+ bookUploadUrl = "http://175.27.224.66:8070"
+ fixedToken = "9ef1895d4b004c98b63159e21aac51da893c23c9" // 图片上传 token
+ GCM_IV_LENGTH = 12
+ GCM_TAG_LENGTH = 16
+)
+
+// NewWebSocketManager 创建新的WebSocket管理器
+func NewWebSocketManager() *WebSocketManager {
+ return &WebSocketManager{
+ clients: make(map[*websocket.Conn]bool),
+ broadcast: make(chan []byte, 256), // 带缓冲的通道
+ }
+}
+
+// handleConnections 处理WebSocket连接
+func (manager *WebSocketManager) handleConnections(w http.ResponseWriter, r *http.Request) {
+ // 升级HTTP连接到WebSocket
+ ws, err := upgrader.Upgrade(w, r, nil)
+ if err != nil {
+ log.Printf("WebSocket升级失败: %v", err)
+ return
+ }
+ defer ws.Close()
+
+ // 注册新客户端
+ manager.mu.Lock()
+ manager.clients[ws] = true
+ manager.mu.Unlock()
+
+ log.Println("新的WebSocket客户端连接")
+
+ // 保持连接并处理消息
+ for {
+ _, message, err := ws.ReadMessage()
+ if err != nil {
+ manager.removeClient(ws)
+ break
+ }
+
+ // 处理客户端消息(示例:记录日志)
+ log.Printf("收到WebSocket消息: %s", message)
+ }
+}
+
+// removeClient 移除断开连接的客户端
+func (manager *WebSocketManager) removeClient(ws *websocket.Conn) {
+ manager.mu.Lock()
+ defer manager.mu.Unlock()
+
+ if _, ok := manager.clients[ws]; ok {
+ ws.Close()
+ delete(manager.clients, ws)
+ log.Println("WebSocket客户端断开连接")
+ }
+}
+
+// handleMessages 处理广播消息
+func (manager *WebSocketManager) handleMessages() {
+ for {
+ msg := <-manager.broadcast
+
+ manager.mu.Lock()
+ for client := range manager.clients {
+ err := client.WriteMessage(websocket.TextMessage, msg)
+ if err != nil {
+ log.Printf("发送WebSocket消息错误: %v", err)
+ manager.removeClient(client)
+ }
+ }
+ manager.mu.Unlock()
+ }
+}
+
+// Broadcast 发送广播消息
+func (manager *WebSocketManager) Broadcast(message interface{}) {
+ msg, err := json.Marshal(message)
+ if err != nil {
+ log.Printf("广播消息编码错误: %v", err)
+ return
+ }
+ manager.broadcast <- msg
+}
+
+func main() {
+ // 1. 初始化SQL监控器
+ InitSQLMonitor(1000) // 最多保存1000条SQL执行记录
+
+ // 2. 初始化数据库控制器
+ //bookCenter, err := NewBookCenterController()
+ //if err != nil {
+ // log.Fatalf("初始化图书中心控制器失败: %v", err)
+ //}
+ //defer bookCenter.db.Close()
+ // ======================= 新增:初始化 ES ============================
+ /////** 旧ES,ES0 **/
+ //esClient, err := es.NewESClient(
+ // []string{"http://103.236.91.138:9200"},
+ // "elastic",
+ // "5mRDIUg52VC0fp14nw-F",
+ //)
+
+ ///** ES,ES1 **/
+ //esClient, err := es.NewESClient(
+ // []string{"http://103.236.74.207:9200"},
+ // "elastic",
+ // "y3h2fdYyrXewkSrY6uhh",
+ //)
+
+ /** 新ES,任务2 **/
+ esClient, err := es.NewESClient(
+ []string{"http://localhost:9200"},
+ "elastic",
+ "zDzSXel3PFwx9=6Ybmqv",
+ )
+ /** ES2 本地测试 **/
+ //esClient, err := es.NewESClient(
+ // []string{"http://36.212.1.63:9200"},
+ // "elastic",
+ // "zDzSXel3PFwx9=6Ybmqv",
+ //)
+ if err != nil {
+ log.Fatalf("初始化 ES 客户端失败: %s", err)
+ }
+
+ esService := es.NewESSearchService(esClient)
+ // ===================================================================
+
+ // 3. 初始化IP日志路径
+ EnsureIPLogPathInitialized()
+ // 4. 创建Gin路由
+ r := gin.Default()
+ // 配置CORS中间件
+ r.Use(cors.New(cors.Config{
+ AllowOrigins: []string{"http://localhost:82", "http://103.236.91.138:9009", "https://test.centerbook.buzhiyushu.cn", "https://centerbook.buzhiyushu.cn"},
+ AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
+ AllowHeaders: []string{
+ "Origin",
+ "Content-Type",
+ "Authorization",
+ "clientid",
+ "Content-Language",
+ "X-Requested-With",
+ },
+ ExposeHeaders: []string{"Content-Length", "Content-Type"}, // ✅ 增加 Content-Type
+ AllowCredentials: true,
+ MaxAge: 1 * time.Second,
+ }))
+ // 统一请求审计日志(记录 endpoint、URI、状态码、IP、UA、耗时)
+ r.Use(RequestAuditLogger())
+ // 未匹配路由统一处理(详细记录 404 来源)
+ r.NoRoute(NotFoundHandler)
+ // 暴露 ip.txt(用于通过反向代理映射到 https://book.center.buzhiyushu.cn/ip.txt)
+ r.GET("/ip.txt", IPLogHandler())
+ // 5. 初始化WebSocket管理器
+ wsManager := NewWebSocketManager()
+ go wsManager.handleMessages()
+ r = gin.Default()
+
+ // 创建限流器
+ //rl := middleware.NewRateLimiter()
+
+ //// 6. 注册API路由
+ ////根据条件查询图书信息
+ //// 单ip限流一分钟两次
+ //r.GET("/api/bookBase/getBookBaseInfo", rl.LimitMiddleware(5, time.Minute), bookCenter.GetBookBaseInfo)
+ ////r.GET("/api/bookBase/getBookBaseInfo", bookCenter.GetBookBaseInfo)
+ ////随机获取图书信息(核价专用)
+ //r.GET("/api/bookBase/getRandomBookBaseInfo", bookCenter.GetRandomBookBaseInfo)
+ ////优化查询图书信息
+ //r.GET("/api/bookBase/GetBookBaseInfoOptimized", bookCenter.GetBookBaseInfoOptimized)
+ ////根据ISBN查询图书信息
+ //r.GET("/api/bookBase/getBookByISBN", bookCenter.GetBookByISBN)
+ ////根据ID查询图书信息
+ //r.GET("/api/bookBase/getBookById", bookCenter.GetBookByID)
+ ////批量修改违规数据
+ //r.POST("/api/bookBase/putBookBaseInfoToIll", bookCenter.SetBookBaseInfoToIll)
+ ////根据ISBN批量修改违规数据
+ //r.POST("/api/bookBase/putBookBaseInfoToIllByISBN", bookCenter.SetBookBaseInfoToIllByISBN)
+ ////修改图书书名
+ //r.POST("/api/bookBase/putBookBaseName", bookCenter.SetBookBaseInfoBookName)
+ ////新增图书数据
+ //r.POST("/api/bookBase/addBookBaseInfo", bookCenter.InsertBaseInfo)
+ ////修改销量数据
+ //r.POST("/api/bookBase/updateSales", bookCenter.UpdateSales)
+ ////根据ISBN获取书籍小图
+ //r.GET("/api/bookBase/getBookPicByISBN", bookCenter.GetBookPicByISBN)
+ ////上传图书图片并更新book_pic_new字段
+ //r.POST("/api/bookBase/uploadBookPic", bookCenter.UploadBookPic)
+ ////根据isbn查询图书信息并存储(xcx使用)
+ //r.GET("/api/bookBase/getBookByIsbnXcx", bookCenter.getBookByIsbnXcx)
+ //r.POST("/api/bookBase/getBookByIsbnXcx", bookCenter.getBookByIsbnXcx)
+ ////选品中心核价码解密
+ //r.GET("/api/bookBase/deleterIsbn", bookCenter.exportISBNs)
+ //r.GET("/api/bookBase/updateBooks", bookCenter.updateBooks)
+ //// ---------------- erp相关 ------------------
+ //erp.RegisterERPRoutes(r, bookCenter.zhishuDB) // ERP 路由注册集中管理
+ //
+ //// =================== ⭐ 挂载 加密 pricingLink 接口 =====================
+ //r.GET("/pricingLink", func(c *gin.Context) {
+ // PricingLinkHandler(c.Writer, c.Request)
+ //})
+ //// =================== ⭐ 挂载 解密 pricingLink 接口 =====================
+ //r.GET("/pricingLinkDec", func(c *gin.Context) {
+ // PricingLinkDecHandler(c.Writer, c.Request)
+ //})
+ //// =================================================================
+ //// 6. 健康检查端点
+ //r.GET("/health", bookCenter.HealthCheck)
+ //r.GET("/ready", bookCenter.ReadyCheck)
+
+ // 7. SQL健康监控端点
+ sqlHealthController := NewSQLHealthController()
+ r.GET("/api/sql-health/stats", sqlHealthController.GetSQLStats)
+ r.GET("/api/sql-health/recent", sqlHealthController.GetRecentSQLRecords)
+ r.GET("/api/sql-health/slow-queries", sqlHealthController.GetSlowQueries)
+ r.GET("/api/sql-health/failed-queries", sqlHealthController.GetFailedQueries)
+ r.POST("/api/sql-health/clear", sqlHealthController.ClearSQLRecords)
+ r.GET("/api/sql-health/dashboard", sqlHealthController.GetSQLHealthDashboard)
+
+ // =================== ⭐ ES =====================
+
+ // ISBN 模糊搜索
+ r.GET("/api/es/searchByISBNLike", esService.SearchBooksHandler)
+ // ISBN 精确搜索
+ r.GET("/api/es/searchByISBN", esService.SearchBookByISBNHandler)
+ // 书名搜索
+ r.GET("/api/es/searchByBookName", esService.SearchBookByBookNameHandler)
+ // 全字段搜索
+ r.GET("/api/es/searchAll", esService.SearchBooksAllFieldsHandler)
+ // 根据条件查询 ES 图书信息
+ r.GET("/api/es/getBookBaseInfoES", esService.SearchBookBaseInfoESHandler)
+ // 新:核价软件用批量获取
+ r.GET("/api/es/batchGetBookBaseInfoES", esService.BatchGetBookBaseInfoESHandler)
+ // 多条件高级搜索
+ r.GET("/api/es/searchAdvanced", esService.SearchBooksByConditionsHandler)
+ // ID 范围计数
+ r.GET("/api/es/countByIDRange", esService.CountByIDRangeHandler)
+ // 根据 ISBN 获取在售数量并更新 ES 的 sell_counts
+ r.POST("/api/es/updateSellCountsByISBN", esService.UpdateSellCountsByISBNHandler)
+ // 新增:按入参直接更新在售数量
+ r.POST("/api/es/updateSellCountsDirect", esService.UpdateSellCountsDirectHandler)
+ // 新增:根据ISBN更新图书的is_suit字段
+ r.POST("/api/es/updateBookSuitByISBN", esService.UpdateBookSuitByISBNHandler)
+ // 新增:根据ISBN通用更新图书字段
+ r.POST("/api/es/updateBookFieldsByISBN", esService.UpdateBookFieldsByISBNHandler)
+ // 新增:根据ISBN查询ES中是否存在,不存在则新增数据,存在则根据参数更新
+ r.POST("/api/es/addBookToES", esService.AddBookToESHandler)
+ // 新增:完整插入接口,支持所有字段,
+ r.POST("/api/es/addBookFullToES", esService.AddBookFullToESHandler)
+ // 新增:批量插入接口,支持同时插入多本图书
+ r.POST("/api/es/batchAddBookToES", esService.BatchAddBookToESHandler)
+ // 新增:根据ISBN检查书籍是否存在
+ r.GET("/api/es/checkBookExists", esService.CheckBookExistsByISBNHandler)
+ r.POST("/api/es/checkBookExists", esService.CheckBookExistsByISBNHandler)
+ // 删除:根据ISBN删除ES数据
+ r.GET("/api/es/DeleteBookByISBN", esService.DeleteBookHandler)
+ // 新增:根据ID删除ES数据
+ r.GET("/api/es/DeleteBookByID", esService.DeleteBookByIDHandler)
+ // 新增:检查书名是否包含套装关键字
+ r.GET("/api/es/checkBookSuit", esService.CheckBookSuitHandler)
+
+ // 8. WebSocket端点
+ r.GET("/ws", func(c *gin.Context) {
+ wsManager.handleConnections(c.Writer, c.Request)
+ })
+ // 4. 启动HTTP服务器
+ port := ":9009"
+ server := &http.Server{
+ Addr: port,
+ Handler: r,
+ }
+
+ go func() {
+ log.Printf("服务器正在监听 %s 端口...", port)
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("服务器启动失败: %v", err)
+ }
+ }()
+
+ // 5. 优雅关闭处理
+ quit := make(chan os.Signal, 1)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+ log.Println("正在关闭服务器...")
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ if err := server.Shutdown(ctx); err != nil {
+ log.Fatal("服务器关闭出错:", err)
+ }
+ log.Println("服务器已成功关闭")
+}
+
+// NewBookCenterController 创建图书中心控制器实例
+func NewBookCenterController() (*BookCenterController, error) {
+ // 数据库连接配置
+ // 资源服务器
+ //targetDSN := "book_center:Ma6XCWE82psHYt3x@tcp(175.27.224.66:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+ // 资源服务器 - 内网连接
+ //targetDSN := "book_center:Ma6XCWE82psHYt3x@tcp(10.206.0.10:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+ // 新数据库
+ targetDSN := "root:Long6166@@@tcp(nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com:26247)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+ // 内网
+ //targetDSN := "root:Long6166@@@tcp(10.206.16.4:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+ // erp数据库
+
+ //查询新后台用户ViP信息
+ //打开中心数据连接
+ db, err := sql.Open("mysql", targetDSN)
+ if err != nil {
+ return nil, fmt.Errorf("数据库连接失败: %v", err)
+ }
+
+ // 测试中心书库连接
+ if err = db.Ping(); err != nil {
+ return nil, fmt.Errorf("数据库连接测试失败: %v", err)
+ }
+
+ // 修改后 - 根据服务器配置调整
+ db.SetMaxOpenConns(150)
+ db.SetMaxIdleConns(50)
+ db.SetConnMaxLifetime(30 * time.Minute)
+ // 新增连接最大空闲时间
+ db.SetConnMaxIdleTime(10 * time.Minute)
+
+ // 初始化 Redis 客户端
+ rdb := redis.NewClient(&redis.Options{
+ Addr: bookBaseRedisurl,
+ Password: "long6166", // 无密码
+ })
+
+ // 测试 Redis 连接
+ ctx := context.Background()
+ _, err = rdb.Ping(ctx).Result()
+ if err != nil {
+ return nil, fmt.Errorf("Redis 连接失败: %v", err)
+ }
+
+ // -----------------------
+ // 3️⃣ 新增 Zhishu 数据库连接
+ // -----------------------
+ dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&allowAllFiles=true",
+ "zhishu", "XsRR4K3ATizyc5BK", "146.56.227.42:3306", "zhishu")
+
+ zhishuDB, err := sql.Open("mysql", dsn)
+ if err != nil {
+ return nil, fmt.Errorf("Zhishu 数据库连接失败: %v", err)
+ }
+
+ zhishuDB.SetMaxOpenConns(150)
+ zhishuDB.SetMaxIdleConns(50)
+ zhishuDB.SetConnMaxLifetime(30 * time.Minute)
+ zhishuDB.SetConnMaxIdleTime(10 * time.Minute)
+
+ if err = zhishuDB.Ping(); err != nil {
+ return nil, fmt.Errorf("Zhishu 数据库连接测试失败: %v", err)
+ }
+ log.Println("Zhishu 数据库连接成功")
+
+ // -----------------------
+ // 4️⃣ 返回控制器实例
+ // -----------------------
+ return &BookCenterController{
+ db: db,
+ cache: cache.New(5*time.Minute, 10*time.Minute),
+ redis: rdb,
+ zhishuDB: zhishuDB, // ✅ 新增字段
+ }, nil
+}
+
+// HealthCheck 综合健康检查
+func (bc *BookCenterController) HealthCheck(c *gin.Context) {
+ // 检查数据库连接
+ if err := bc.db.Ping(); err != nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "status": "down",
+ "message": "数据库连接失败",
+ "error": err.Error(),
+ })
+ return
+ }
+
+ // 检查缓存状态
+ bc.cache.Set("healthcheck", "ok", 1*time.Minute)
+ if _, found := bc.cache.Get("healthcheck"); !found {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "status": "down",
+ "message": "缓存服务异常",
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "up",
+ "message": "服务运行正常",
+ "details": gin.H{
+ "database": "connected",
+ "cache": "working",
+ "time": time.Now().Format(time.RFC3339),
+ },
+ })
+}
+
+// ReadyCheck 就绪检查
+func (bc *BookCenterController) ReadyCheck(c *gin.Context) {
+ // 检查数据库连接
+ if err := bc.db.Ping(); err != nil {
+ c.JSON(http.StatusServiceUnavailable, gin.H{
+ "status": "not_ready",
+ "message": "数据库连接失败",
+ "error": err.Error(),
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "status": "ready",
+ "message": "服务准备就绪",
+ })
+}
+
+// GetBookBaseInfo 条件查询+(缓存15分钟)
+// 优化点:
+// 1) 计数查询不再携带 ORDER BY,避免不必要的排序开销;
+// 2) 在子查询中加入 FORCE INDEX(idx_search_limit) 引导优化器使用覆盖索引;
+// 3) 采用子查询筛选 id + 主表 JOIN 的方式返回字段,兼顾分页与排序性能。
+func (bc *BookCenterController) GetBookBaseInfo(c *gin.Context) {
+ //在控制台打印访问信息(合并IP与查询参数)
+ log.Printf("GetBookBaseInfo 请求 | IP: %s | 参数: %v", c.ClientIP(), c.Request.URL.Query())
+
+ // 启动计时器
+ startTime := time.Now()
+
+ // 生成缓存键
+ cacheKey := generateCacheKey(c.Request.URL.Query())
+
+ // 尝试从缓存获取
+ if cachedData, found := bc.cache.Get(cacheKey); found {
+ c.JSON(http.StatusOK, cachedData)
+ return
+ }
+
+ saleSelect := c.DefaultQuery("saleSelect", "2")
+
+ // 根据查询参数动态选择索引提示(避免错误强制使用不匹配索引)
+ // 当筛选包含具体的销量字段时,优先选择以该字段为前缀的复合索引
+ var chosenIndex string
+ switch saleSelect {
+ case "7":
+ chosenIndex = "idx_core_search" // day_sale_7 为前缀
+ case "15":
+ chosenIndex = "idx_day_sale_15_search"
+ case "30":
+ chosenIndex = "idx_day_sale_30_search"
+ case "60":
+ // 如需,可建立对应 day_sale_60 索引;暂不强制
+ chosenIndex = ""
+ case "90":
+ chosenIndex = ""
+ case "180":
+ // idx_core_search 内含 day_sale_180,但前缀是 day_sale_7,这里不强制
+ chosenIndex = ""
+ case "365":
+ chosenIndex = ""
+ default:
+ // 未选择具体销量窗口,但若存在数量筛选,优先数量复合索引
+ if c.Query("sell_counts") != "" || c.Query("buy_counts") != "" {
+ chosenIndex = "idx_query_opt"
+ }
+ }
+ // 记录索引提示选择,便于线上观测
+ if chosenIndex != "" {
+ log.Printf("索引提示选择: %s", chosenIndex)
+ } else {
+ log.Printf("索引提示选择: 无(交由优化器决定)")
+ }
+ // 构建子查询 - 用于快速筛选ID
+ // 按参数动态添加索引提示
+ subQuery := "SELECT id FROM book_center"
+ if chosenIndex != "" {
+ subQuery += " FORCE INDEX (" + chosenIndex + ")"
+ }
+ subQuery += " WHERE 1=1"
+ var subArgs []interface{}
+
+ // 设置默认过滤条件(确保参数顺序稳定)
+ // 注意:Go 的 map 迭代顺序不稳定,这里用有序切片保证追加顺序
+ defaultOrder := []struct {
+ field string
+ value interface{}
+ }{
+ {field: "vio_book", value: 0},
+ {field: "book_set", value: 0},
+ {field: "onenum_mbooks", value: 0},
+ {field: "ill_publisher", value: 0},
+ {field: "ill_author", value: 0},
+ }
+ // 传入isbn时只查询isbn,其他参数不作为参数查询数据库
+ hasISBN := c.Query("isbn") != ""
+ var conditionOrder []string
+ if !hasISBN {
+ for _, item := range defaultOrder {
+ userValue := c.Query(item.field)
+ if userValue == "" {
+ subQuery += fmt.Sprintf(" AND %s = ?", item.field)
+ subArgs = append(subArgs, item.value)
+ conditionOrder = append(conditionOrder, item.field)
+ }
+ }
+ }
+
+ // 定义各种查询字段类型
+ exactFields := []string{
+ "id", "isbn", "publisher",
+ }
+
+ // 根据 saleSelect 决定使用哪个字段作为销售数量字段
+ var saleCountField string
+ switch saleSelect {
+ case "7":
+ saleCountField = "day_sale_7"
+ case "15":
+ saleCountField = "day_sale_15"
+ case "30":
+ saleCountField = "day_sale_30"
+ case "60":
+ saleCountField = "day_sale_60"
+ case "90":
+ saleCountField = "day_sale_90"
+ case "180":
+ saleCountField = "day_sale_180"
+ case "365":
+ saleCountField = "day_sale_365"
+ case "0":
+ saleCountField = "this_year_sale"
+ case "1":
+ saleCountField = "last_year_sale"
+ default:
+ saleCountField = "buy_counts"
+ }
+
+ numericRangeFields := []string{"buy_counts", "sell_counts", "day_sale_7", "day_sale_15", "day_sale_30", "day_sale_60", "day_sale_90", "day_sale_180", "day_sale_365", "this_year_sale", "last_year_sale"}
+ boolFields := []string{"book_pic", "vio_book", "book_set", "onenum_mbooks", "ill_publisher", "ill_author"}
+ dateFields := []string{"print_time", "publiction_times"}
+ camelToSnakeMap := map[string]string{
+ "bookName": "book_name",
+ "vioBook": "vio_book",
+ "bookSet": "book_set",
+ "onenumMbooks": "onenum_mbooks",
+ "illPublisher": "ill_publisher",
+ "illAuthor": "ill_author",
+ "bookPic": "book_pic",
+ }
+
+ // 定义要排除的参数名
+ excludedParams := []string{"page", "per_page", "is_pricing", "saleSelect", "last_id", "pageNum", "pageSize"}
+
+ // 首先应用默认过滤条件(按照固定顺序)
+ // 同时记录 WHERE 条件追加顺序,便于排查与索引前缀不一致的问题
+ //var conditionOrder []string
+ //for _, item := range defaultOrder {
+ // userValue := c.Query(item.field)
+ // if userValue == "" {
+ // subQuery += fmt.Sprintf(" AND %s = ?", item.field)
+ // subArgs = append(subArgs, item.value)
+ // conditionOrder = append(conditionOrder, item.field)
+ // }
+ //}
+
+ // 处理查询参数 - 构建子查询条件(保证稳定顺序)
+ // 1) 优先处理与索引前缀相关的标志位与销量/数量字段
+ // 2) 然后处理分类/图片等残余过滤
+ // 3) 最后处理其它精确与日期/模糊字段
+ priorityOrder := []string{
+ // flags
+ "vio_book", "book_set", "onenum_mbooks", "ill_publisher", "ill_author",
+ // sales/counts
+ "day_sale_7", "day_sale_15", "day_sale_30", "day_sale_60", "day_sale_90", "day_sale_180", "day_sale_365", "this_year_sale", "last_year_sale",
+ "sell_counts", "buy_counts",
+ // residual filters often used
+ "category", "book_pic",
+ // exacts
+ "id", "isbn", "publisher",
+ // text
+ "author", "book_name",
+ // dates
+ "print_time", "publiction_times",
+ }
+ // 收集所有请求参数键
+ rawParams := c.Request.URL.Query()
+ used := make(map[string]bool)
+ // 按优先级构造有序键列表
+ var orderedKeys []string
+ for _, k := range priorityOrder {
+ if v, ok := rawParams[k]; ok && len(v) > 0 && v[0] != "" && !contains(excludedParams, k) {
+ orderedKeys = append(orderedKeys, k)
+ used[k] = true
+ }
+ }
+ // 将剩余键按字典序加入(保证稳定)
+ for k, v := range rawParams {
+ if used[k] || contains(excludedParams, k) || len(v) == 0 || v[0] == "" {
+ continue
+ }
+ orderedKeys = append(orderedKeys, k)
+ }
+ sort.Strings(orderedKeys[len(orderedKeys)-len(orderedKeys):]) // no-op for clarity; keys已按优先级+余下加入
+
+ if hasISBN {
+ // 仅允许 ISBN,不允许任何过滤字段介入
+ orderedKeys = []string{"isbn"}
+ }
+
+ // 按有序键处理
+ for _, key := range orderedKeys {
+ values := rawParams[key]
+ value := values[0]
+ // 处理驼峰参数转换为下划线
+ if dbField, ok := camelToSnakeMap[key]; ok {
+ key = dbField
+ }
+
+ // 特殊处理book_pic参数
+ if key == "book_pic" {
+ if value == "0" {
+ subQuery += " AND (book_pic IS NULL OR book_pic = '')"
+ } else if value == "1" {
+ subQuery += " AND (book_pic IS NOT NULL AND book_pic != '')"
+ }
+ conditionOrder = append(conditionOrder, key)
+ continue
+ }
+
+ // 特殊处理category参数
+ if key == "category" {
+ // 对URL编码的值进行解码
+ decodedValue, err := url.QueryUnescape(value)
+ if err != nil {
+ decodedValue = value
+ }
+
+ // 特殊处理:排除大学教材
+ if decodedValue == "排除大学教材" {
+ subQuery += " AND category != ?"
+ subArgs = append(subArgs, "图书/教材教辅考试/大学教材")
+ } else {
+ // 使用LIKE进行模糊匹配
+ subQuery += " AND category LIKE ?"
+ subArgs = append(subArgs, "%"+decodedValue+"%")
+ }
+ conditionOrder = append(conditionOrder, key)
+ continue
+ }
+
+ // 特殊处理buy_count参数 - 根据saleSelect使用不同的字段
+ if key == "buy_counts" {
+ key = saleCountField // 替换为动态的销售字段
+ }
+
+ if contains(exactFields, key) {
+ subQuery += fmt.Sprintf(" AND %s = ?", key)
+ subArgs = append(subArgs, value)
+ conditionOrder = append(conditionOrder, key)
+ continue
+ }
+
+ if contains(numericRangeFields, key) {
+ // 特殊处理:如果buy_counts或sell_counts为0到999999的范围,则跳过不加入查询条件
+ if (key == "buy_counts" || key == "sell_counts") && strings.Contains(value, ",") {
+ parts := strings.Split(value, ",")
+ if len(parts) == 2 && parts[0] == "0" && parts[1] == "999999" {
+ // 跳过0到999999的范围查询,不加入WHERE条件
+ conditionOrder = append(conditionOrder, key)
+ continue
+ }
+ }
+
+ // 所有数值字段都使用>=条件,不使用BETWEEN,以便更好地利用索引
+ if strings.Contains(value, ",") {
+ parts := strings.Split(value, ",")
+ if len(parts) == 2 {
+ // 如果是类似"0,999999"的格式,只使用第一个值作为大于等于条件
+ subQuery += fmt.Sprintf(" AND %s >= ?", key)
+ if num, err := strconv.Atoi(parts[0]); err == nil {
+ subArgs = append(subArgs, num)
+ } else {
+ subArgs = append(subArgs, parts[0])
+ }
+ }
+ } else {
+ subQuery += fmt.Sprintf(" AND %s >= ?", key)
+ subArgs = append(subArgs, value)
+ conditionOrder = append(conditionOrder, key)
+ }
+ continue
+ }
+
+ if contains(boolFields, key) {
+ if value == "0" {
+ subQuery += fmt.Sprintf(" AND %s = 0", key)
+ } else if value == "1" {
+ subQuery += fmt.Sprintf(" AND %s = 1", key)
+ }
+ continue
+ }
+
+ if contains(dateFields, key) {
+ if strings.Contains(value, ",") {
+ parts := strings.Split(value, ",")
+ if len(parts) == 2 {
+ subQuery += fmt.Sprintf(" AND %s BETWEEN ? AND ?", key)
+ subArgs = append(subArgs, parts[0], parts[1])
+ }
+ } else {
+ subQuery += fmt.Sprintf(" AND %s >= ? AND %s < DATE_ADD(?, INTERVAL 1 DAY)", key, key)
+ subArgs = append(subArgs, value, value)
+ }
+ continue
+ }
+ // 特殊处理书名和作者的全模糊查询
+ if key == "book_name" || key == "author" {
+ subQuery += fmt.Sprintf(" AND %s LIKE ?", key)
+ subArgs = append(subArgs, "%"+value+"%") // 全模糊匹配
+ continue
+ }
+
+ // 对于已知前缀的查询使用索引
+ if strings.HasPrefix(value, "%") {
+ subQuery += fmt.Sprintf(" AND %s LIKE ?", key)
+ } else {
+ subQuery += fmt.Sprintf(" AND %s LIKE ?", key)
+ value = value + "%"
+ }
+ subArgs = append(subArgs, value)
+ }
+
+ // 处理分页
+ perPage, err := getIntParam(c, "per_page", "pageSize", 10)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "每页数量参数无效"})
+ return
+ }
+ origPerPage := perPage
+ perPage = sanitizePerPage(perPage)
+ if perPage != origPerPage {
+ log.Printf("per_page 超出限制: %d -> %d (已裁剪)", origPerPage, perPage)
+ LogLargePerPage(c, origPerPage, perPage)
+ }
+
+ page, err := getInt64Param(c, "page", "pageNum", 1)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "页码参数无效"})
+ return
+ }
+
+ // 如果请求的 per_page 超过上限,则直接返回空数组,避免重查询
+ if origPerPage != perPage {
+ c.JSON(http.StatusOK, gin.H{
+ "data": []map[string]interface{}{},
+ "total": 0,
+ "per_page": origPerPage,
+ "current_page": page,
+ })
+ return
+ }
+
+ // 为 COUNT 查询单独保留一个不含排序与分页的过滤语句
+ filterQuery := subQuery
+
+ // 子查询添加排序和分页(仅用于实际数据查询)
+ subQuery = filterQuery + " ORDER BY id DESC"
+ offset := (page - 1) * int64(perPage)
+ subQuery += " LIMIT ? OFFSET ?"
+ subArgs = append(subArgs, perPage, offset)
+
+ // 构建最终的优化查询 - 使用子查询+JOIN
+ finalQuery := `SELECT
+ bc.id,
+ bc.book_name,
+ bc.book_pic,
+ bc.isbn,
+ bc.author,
+ bc.category,
+ bc.publisher,
+ bc.publication_time,
+ bc.binding_layout,
+ bc.fix_price,
+ bc.buy_counts,
+ bc.sell_counts,
+ bc.vio_book,
+ bc.book_set,
+ bc.onenum_mbooks,
+ bc.ill_publisher,
+ bc.ill_author,
+ bc.day_sale_7,
+ bc.day_sale_15,
+ bc.day_sale_30,
+ bc.day_sale_60,
+ bc.day_sale_90,
+ bc.day_sale_180,
+ bc.day_sale_365,
+ bc.this_year_sale,
+ bc.last_year_sale,
+ bc.total_sale,
+ bc.update_time,
+ bc.book_pic_new
+ FROM (` + subQuery + `) AS ids
+ JOIN book_center bc ON ids.id = bc.id
+ ORDER BY bc.id DESC`
+
+ // 打印执行的SQL语句和参数
+ log.Printf("执行优化SQL查询: %s", finalQuery)
+ log.Printf("SQL参数: %v 访问ip:%s", subArgs, c.ClientIP())
+
+ // 执行查询
+ stmt, err := bc.db.Prepare(finalQuery)
+ if err != nil {
+ log.Printf("Prepare statement error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "准备查询语句失败"})
+ return
+ }
+ defer stmt.Close()
+
+ var rows *sql.Rows
+ rows, err = stmt.Query(subArgs...)
+ if err != nil {
+ maxRetries := 3
+ for i := 0; i < maxRetries; i++ {
+ rows, err = stmt.Query(subArgs...)
+ if err == nil {
+ break
+ }
+ time.Sleep(time.Second * time.Duration(i+1))
+ }
+ if err != nil {
+ log.Printf("Database query error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "数据库查询失败",
+ "details": err.Error(),
+ "query": finalQuery,
+ })
+ return
+ }
+ }
+ defer rows.Close()
+
+ // 处理查询结果
+ columns, err := rows.Columns()
+ if err != nil {
+ log.Printf("Get columns error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "获取列信息失败"})
+ return
+ }
+
+ var results []map[string]interface{}
+ for rows.Next() {
+ values := make([]interface{}, len(columns))
+ pointers := make([]interface{}, len(columns))
+ for i := range values {
+ pointers[i] = &values[i]
+ }
+
+ if err := rows.Scan(pointers...); err != nil {
+ log.Printf("Row scan error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "读取行数据失败"})
+ return
+ }
+
+ row := make(map[string]interface{})
+ for i, col := range columns {
+ if col == "publication_time" {
+ if values[i] != nil {
+ switch v := values[i].(type) {
+ case int64:
+ t := time.Unix(v, 0)
+ row[col] = t.Format("2006-01")
+ case []byte:
+ timeStr := string(v)
+ if t, err := time.Parse("2006-01-02", timeStr); err == nil {
+ row[col] = t.Format("2006-01")
+ } else if t, err := time.Parse(time.RFC3339, timeStr); err == nil {
+ row[col] = t.Format("2006-01")
+ } else {
+ row[col] = timeStr
+ }
+ default:
+ row[col] = values[i]
+ }
+ } else {
+ row[col] = nil
+ }
+ } else if col == "update_time" {
+ if values[i] != nil {
+ switch v := values[i].(type) {
+ case int64:
+ // 处理时间戳,转换为标准时间格式
+ t := time.Unix(v, 0)
+ row[col] = t.Format("2006-01-02 15:04:05")
+ case []byte:
+ // 处理字符串形式的时间戳
+ timeStr := string(v)
+ if timestamp, err := strconv.ParseInt(timeStr, 10, 64); err == nil {
+ t := time.Unix(timestamp, 0)
+ row[col] = t.Format("2006-01-02 15:04:05")
+ } else {
+ row[col] = timeStr
+ }
+ case string:
+ // 处理字符串形式的时间戳
+ if timestamp, err := strconv.ParseInt(v, 10, 64); err == nil {
+ t := time.Unix(timestamp, 0)
+ row[col] = t.Format("2006-01-02 15:04:05")
+ } else {
+ row[col] = v
+ }
+ default:
+ row[col] = values[i]
+ }
+ } else {
+ row[col] = nil
+ }
+ } else {
+ if b, ok := values[i].([]byte); ok {
+ row[col] = string(b)
+ } else {
+ row[col] = values[i]
+ }
+ }
+ }
+ results = append(results, row)
+ }
+
+ if err := rows.Err(); err != nil {
+ log.Printf("Rows error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "处理结果集时出错"})
+ return
+ }
+
+ // 获取总数 - 使用不带 ORDER BY/LIMIT 的过滤查询,避免排序开销
+ log.Printf("执行计数过滤SQL: %s", filterQuery)
+ countQuery := "SELECT COUNT(*) FROM (" + filterQuery + ") AS count_table"
+ var total int
+ countArgs := subArgs[:len(subArgs)-2]
+
+ // 打印总数查询的SQL语句和参数
+ log.Printf("执行总数查询SQL: %s", countQuery)
+ log.Printf("总数查询SQL参数: %v", countArgs)
+
+ if err := bc.db.QueryRow(countQuery, countArgs...).Scan(&total); err != nil {
+ log.Printf("Count query error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "获取总数失败"})
+ return
+ }
+ if results == nil {
+ results = []map[string]interface{}{}
+ }
+ // 构建响应数据
+ responseData := gin.H{
+ "data": results,
+ "total": total,
+ "per_page": perPage,
+ "current_page": page,
+ }
+
+ // 将结果存入缓存,有效期15分钟
+ bc.cache.Set(cacheKey, responseData, 15*time.Minute)
+
+ // 记录查询时间
+ elapsed := time.Since(startTime)
+ // 输出WHERE条件追加顺序,辅助分析索引前缀匹配情况
+ log.Printf("WHERE 条件顺序: %v", conditionOrder)
+ log.Printf("Optimized query executed in %v", elapsed)
+
+ c.JSON(http.StatusOK, responseData)
+}
+
+// 将下划线命名转换为驼峰命名
+//func toCamelCase(s string) string {
+// parts := strings.Split(s, "_")
+// for i := 1; i < len(parts); i++ {
+// parts[i] = strings.Title(parts[i])
+// }
+// return strings.Join(parts, "")
+//}
+
+// generateCacheKey 生成唯一的缓存键}
+
+// GetBookBaseInfoOptimized 优化的图书基础信息查询方法
+// 根据是否传递销量相关参数区分普通查询和高级搜索
+func (bc *BookCenterController) GetBookBaseInfoOptimized(c *gin.Context) {
+ startTime := time.Now()
+
+ // 生成缓存键
+ cacheKey := generateCacheKey(c.Request.URL.Query())
+
+ // 尝试从缓存获取
+ if cachedData, found := bc.cache.Get(cacheKey); found {
+ c.JSON(http.StatusOK, cachedData)
+ return
+ }
+
+ queryParams := c.Request.URL.Query()
+
+ // 获取分页参数
+ page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
+ pageSize, _ := strconv.Atoi(c.DefaultQuery("per_page", "20"))
+ if page < 1 {
+ page = 1
+ }
+ if pageSize < 1 || pageSize > 100 {
+ pageSize = 20
+ }
+ offset := (page - 1) * pageSize
+
+ log.Printf("查询参数: %v, 分页: page=%d, per_page=%d", queryParams, page, pageSize)
+
+ //----------------------------------高级搜索------------------------------
+ subQuery := "WHERE 1=1"
+ subArgs := []interface{}{}
+
+ // saleSelect 决定销量字段
+ saleSelect := c.DefaultQuery("saleSelect", "2")
+ var saleCountField string
+ switch saleSelect {
+ case "7":
+ saleCountField = "day_sale_7"
+ case "15":
+ saleCountField = "day_sale_15"
+ case "30":
+ saleCountField = "day_sale_30"
+ case "60":
+ saleCountField = "day_sale_60"
+ case "90":
+ saleCountField = "day_sale_90"
+ case "180":
+ saleCountField = "day_sale_180"
+ case "365":
+ saleCountField = "day_sale_365"
+ default:
+ saleCountField = ""
+ }
+
+ // 1️⃣ 销量字段筛选
+ // ✅ 仅当 saleCountField 不为空时才拼接条件
+ if saleCountField != "" {
+ subQuery += fmt.Sprintf(" AND %s >= ?", saleCountField)
+ subArgs = append(subArgs, 1)
+ }
+ // 2️⃣ isbn
+ if v := queryParams.Get("isbn"); v != "" {
+ subQuery += " AND isbn = ?"
+ subArgs = append(subArgs, v)
+ }
+ // book_name
+ if v := queryParams.Get("book_name"); v != "" {
+ subQuery += " AND book_name = ?"
+ subArgs = append(subArgs, v)
+ }
+ // author
+ if v := queryParams.Get("author"); v != "" {
+ subQuery += " AND author = ?"
+ subArgs = append(subArgs, v)
+ }
+ // publisher
+ if v := queryParams.Get("publisher"); v != "" {
+ subQuery += " AND publisher = ?"
+ subArgs = append(subArgs, v)
+ }
+ // 3️⃣ bookPic
+ if v := queryParams.Get("book_pic"); v != "" {
+ switch v {
+ case "1":
+ subQuery += " AND book_pic > ''" // 能用索引
+ case "0":
+ subQuery += " AND book_pic = ''"
+ }
+ }
+
+ // 2️⃣ vio_book
+ if v := queryParams.Get("vio_book"); v != "" {
+ subQuery += " AND vio_book = ?"
+ subArgs = append(subArgs, v)
+ }
+
+ // 3️⃣ book_set
+ if v := queryParams.Get("book_set"); v != "" {
+ subQuery += " AND book_set = ?"
+ subArgs = append(subArgs, v)
+ }
+
+ // 4️⃣ onenum_mbooks
+ if v := queryParams.Get("onenum_mbooks"); v != "" {
+ subQuery += " AND onenum_mbooks = ?"
+ subArgs = append(subArgs, v)
+ }
+
+ // 5️⃣ ill_publisher
+ if v := queryParams.Get("ill_publisher"); v != "" {
+ subQuery += " AND ill_publisher = ?"
+ subArgs = append(subArgs, v)
+ }
+
+ // 6️⃣ ill_author
+ if v := queryParams.Get("ill_author"); v != "" {
+ subQuery += " AND ill_author = ?"
+ subArgs = append(subArgs, v)
+ }
+
+ // 7️⃣ category
+ if v := queryParams.Get("category"); v != "" {
+ if v == "排除大学教材" {
+ subQuery += " AND category != ?"
+ subArgs = append(subArgs, "图书/教材教辅考试/大学教材")
+ } else {
+ subQuery += " AND category = ?"
+ subArgs = append(subArgs, v)
+ }
+ }
+
+ // 8️⃣ buy_counts
+ if vals := queryParams["buy_counts"]; len(vals) > 0 {
+ parts := strings.Split(vals[0], ",")
+ if len(parts) > 0 {
+ subQuery += " AND buy_counts >= ?"
+ subArgs = append(subArgs, parts[0])
+ }
+ }
+
+ // 9️⃣ sell_counts
+ if vals := queryParams["sell_counts"]; len(vals) > 0 {
+ parts := strings.Split(vals[0], ",")
+ if len(parts) > 0 {
+ subQuery += " AND sell_counts >= ?"
+ subArgs = append(subArgs, parts[0])
+ }
+ }
+
+ log.Println("最终 SQL 条件:", subQuery)
+
+ // ------------------ 统计总数 ------------------
+ countQuery := "SELECT COUNT(id) FROM book_center " + subQuery
+ log.Printf("查询总数 SQL: %s, 参数: %v", countQuery, subArgs)
+
+ var total int
+ if err := bc.db.QueryRow(countQuery, subArgs...).Scan(&total); err != nil {
+ log.Printf("Count query error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "获取总数失败"})
+ return
+ }
+
+ // ------------------ 查询数据列表 ------------------
+ fields := `id, book_name, book_pic, isbn, author, category, publisher,
+ publication_time, binding_layout, fix_price, buy_counts, sell_counts,
+ vio_book, book_set, onenum_mbooks, ill_publisher, ill_author,
+ day_sale_7, day_sale_15, day_sale_30, day_sale_60, day_sale_90,
+ day_sale_180, day_sale_365, this_year_sale, last_year_sale,
+ total_sale, update_time`
+
+ dataQuery := fmt.Sprintf("SELECT %s FROM book_center %s ORDER BY id DESC LIMIT ? OFFSET ?", fields, subQuery)
+ subArgsWithLimit := append(subArgs, pageSize, offset)
+ log.Printf("查询数据 SQL: %s, 参数: %v", dataQuery, subArgsWithLimit)
+
+ rows, err := bc.db.Query(dataQuery, subArgsWithLimit...)
+ if err != nil {
+ log.Printf("Data query error: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "查询数据失败"})
+ return
+ }
+ defer rows.Close()
+
+ var results []map[string]interface{}
+ for rows.Next() {
+ var book struct {
+ ID int64 `db:"id"`
+ BookName string `db:"book_name"`
+ BookPic string `db:"book_pic"`
+ ISBN string `db:"isbn"`
+ Author string `db:"author"`
+ Category string `db:"category"`
+ Publisher string `db:"publisher"`
+ PublicationTime *int64 `db:"publication_time"`
+ BindingLayout string `db:"binding_layout"`
+ FixPrice *int `db:"fix_price"`
+ BuyCounts *int64 `db:"buy_counts"`
+ SellCounts *int64 `db:"sell_counts"`
+ VioBook int `db:"vio_book"`
+ BookSet int `db:"book_set"`
+ OnenumMbooks *int `db:"onenum_mbooks"`
+ IllPublisher int `db:"ill_publisher"`
+ IllAuthor int `db:"ill_author"`
+ DaySale7 int `db:"day_sale_7"`
+ DaySale15 int `db:"day_sale_15"`
+ DaySale30 int `db:"day_sale_30"`
+ DaySale60 int `db:"day_sale_60"`
+ DaySale90 int `db:"day_sale_90"`
+ DaySale180 int `db:"day_sale_180"`
+ DaySale365 int `db:"day_sale_365"`
+ ThisYearSale int `db:"this_year_sale"`
+ LastYearSale int `db:"last_year_sale"`
+ TotalSale int `db:"total_sale"`
+ UpdateTime int64 `db:"update_time"`
+ }
+
+ if err := rows.Scan(
+ &book.ID, &book.BookName, &book.BookPic, &book.ISBN, &book.Author,
+ &book.Category, &book.Publisher, &book.PublicationTime, &book.BindingLayout,
+ &book.FixPrice, &book.BuyCounts, &book.SellCounts, &book.VioBook,
+ &book.BookSet, &book.OnenumMbooks, &book.IllPublisher, &book.IllAuthor,
+ &book.DaySale7, &book.DaySale15, &book.DaySale30, &book.DaySale60,
+ &book.DaySale90, &book.DaySale180, &book.DaySale365, &book.ThisYearSale,
+ &book.LastYearSale, &book.TotalSale, &book.UpdateTime,
+ ); err != nil {
+ log.Printf("Row scan error: %v", err)
+ continue
+ }
+
+ // 格式化 publication_time
+ var publicationTimeStr string
+ if book.PublicationTime != nil {
+ t := time.Unix(*book.PublicationTime, 0)
+ publicationTimeStr = t.Format("2006-01")
+ }
+
+ // 格式化 update_time
+ updateTimeStr := ""
+ if book.UpdateTime != 0 {
+ t := time.Unix(book.UpdateTime, 0)
+ updateTimeStr = t.Format("2006-01-02 15:04:05")
+ }
+
+ // 转换为与示例完全一致的格式
+ bookMap := map[string]interface{}{
+ "id": book.ID,
+ "book_name": book.BookName,
+ "book_pic": book.BookPic,
+ "isbn": book.ISBN,
+ "author": book.Author,
+ "category": book.Category,
+ "publisher": book.Publisher,
+ "publication_time": publicationTimeStr,
+ "binding_layout": book.BindingLayout,
+ "fix_price": book.FixPrice,
+ "buy_counts": book.BuyCounts,
+ "sell_counts": book.SellCounts,
+ "vio_book": book.VioBook,
+ "book_set": book.BookSet,
+ "onenum_mbooks": book.OnenumMbooks,
+ "ill_publisher": book.IllPublisher,
+ "ill_author": book.IllAuthor,
+ "day_sale_7": book.DaySale7,
+ "day_sale_15": book.DaySale15,
+ "day_sale_30": book.DaySale30,
+ "day_sale_60": book.DaySale60,
+ "day_sale_90": book.DaySale90,
+ "day_sale_180": book.DaySale180,
+ "day_sale_365": book.DaySale365,
+ "this_year_sale": book.ThisYearSale,
+ "last_year_sale": book.LastYearSale,
+ "total_sale": book.TotalSale,
+ "update_time": updateTimeStr,
+ }
+
+ results = append(results, bookMap)
+ }
+
+ if err = rows.Err(); err != nil {
+ log.Printf("Rows iteration error: %v", err)
+ }
+
+ elapsed := time.Since(startTime)
+ log.Printf("执行耗时: %v, 总数: %d, 返回数据: %d 条", elapsed, total, len(results))
+
+ // 返回结果
+ responseData := gin.H{
+ "current_page": page,
+ "data": results,
+ "per_page": pageSize,
+ "total": total,
+ }
+
+ // 缓存 15 分钟
+ bc.cache.Set(cacheKey, responseData, 15*time.Minute)
+
+ c.JSON(http.StatusOK, responseData)
+}
+
+func generateCacheKey(values url.Values) string {
+ // 复制并排序查询参数以确保相同的查询生成相同的键
+ params := make(url.Values)
+ for k, v := range values {
+ params[k] = v
+ }
+
+ // 对参数进行排序
+ var keys []string
+ for k := range params {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ // 构建缓存键
+ var buf bytes.Buffer
+ for _, k := range keys {
+ vs := params[k]
+ buf.WriteString(k)
+ buf.WriteString("=")
+ buf.WriteString(strings.Join(vs, ","))
+ buf.WriteString("&")
+ }
+
+ // 使用MD5哈希生成固定长度的键
+ hash := md5.Sum(buf.Bytes())
+ return fmt.Sprintf("book_query:%x", hash)
+}
+
+// getIntParam 获取整数参数
+func getIntParam(c *gin.Context, primaryParam string, secondaryParam string, defaultValue int) (int, error) {
+ paramStr := c.Query(primaryParam)
+ if paramStr == "" {
+ paramStr = c.Query(secondaryParam)
+ }
+ if paramStr == "" {
+ return defaultValue, nil
+ }
+ return strconv.Atoi(paramStr)
+}
+
+// getInt64Param 获取64位整数参数
+func getInt64Param(c *gin.Context, primaryParam string, secondaryParam string, defaultValue int64) (int64, error) {
+ paramStr := c.Query(primaryParam)
+ if paramStr == "" {
+ paramStr = c.Query(secondaryParam)
+ }
+ if paramStr == "" {
+ return defaultValue, nil
+ }
+ return strconv.ParseInt(paramStr, 10, 64)
+}
+
+// sanitizePerPage 校验并限制 per_page 的范围,防止过大导致数据库压力。
+// 返回值:合法的每页数量,若超出上限则裁剪到最大值。
+func sanitizePerPage(n int) int {
+ if n <= 0 {
+ return 10
+ }
+ const maxPerPage = 1000
+ if n > maxPerPage {
+ return maxPerPage
+ }
+ return n
+}
+
+// contains 检查字符串是否在切片中
+func contains(slice []string, item string) bool {
+ for _, s := range slice {
+ if s == item {
+ return true
+ }
+ }
+ return false
+}
+
+// GetRandomBookBaseInfo 条件查询(随机,不需要加入缓存)
+func (bc *BookCenterController) GetRandomBookBaseInfo(c *gin.Context) {
+ bc.GetBookBaseInfo(c)
+ // 启动计时器
+ //startTime := time.Now()
+ //
+ //saleSelect := c.DefaultQuery("saleSelect", "2")
+ //
+ //// 初始化查询语句和参数
+ //baseQuery := "SELECT id,book_name,isbn,book_set,publisher,author FROM book_center WHERE 1=1"
+ //
+ //var args []interface{}
+ //
+ //// 定义各种查询字段类型
+ //exactFields := []string{
+ // "id", "isbn", "publisher",
+ //}
+ //
+ //// 根据 saleSelect 决定使用哪个字段作为销售数量字段
+ //var saleCountField string
+ //switch saleSelect {
+ //case "7":
+ // saleCountField = "day_sale_7"
+ //case "15":
+ // saleCountField = "day_sale_15"
+ //case "30":
+ // saleCountField = "day_sale_30"
+ //case "60":
+ // saleCountField = "day_sale_60"
+ //case "90":
+ // saleCountField = "day_sale_90"
+ //case "180":
+ // saleCountField = "day_sale_180"
+ //case "365":
+ // saleCountField = "day_sale_365"
+ //case "0":
+ // saleCountField = "this_year_sale"
+ //case "1":
+ // saleCountField = "last_year_sale"
+ //default:
+ // saleCountField = "buy_counts"
+ //}
+ //
+ //numericRangeFields := []string{"buy_counts", "sell_counts", "day_sale_7", "day_sale_15", "day_sale_30", "day_sale_60", "day_sale_90", "day_sale_180", "day_sale_365", "this_year_sale", "last_year_sale"}
+ //boolFields := []string{"book_pic", "vio_book", "book_set", "onenum_mbooks", "ill_publisher", "ill_author"}
+ //dateFields := []string{"print_time", "publiction_times"}
+ //camelToSnakeMap := map[string]string{
+ // "bookName": "book_name",
+ // "vioBook": "vio_book",
+ // "bookSet": "book_set",
+ // "onenumMbooks": "onenum_mbooks",
+ // "illPublisher": "ill_publisher",
+ // "illAuthor": "ill_author",
+ // "bookPic": "book_pic",
+ //}
+ //
+ //// 定义要排除的参数名
+ //excludedParams := []string{"page", "per_page", "is_pricing", "saleSelect"}
+ //
+ //// 处理查询参数
+ //query := baseQuery
+ //for key, values := range c.Request.URL.Query() {
+ // if contains(excludedParams, key) {
+ // continue
+ // }
+ // if len(values) == 0 || values[0] == "" {
+ // continue
+ // }
+ //
+ // value := values[0]
+ // // 处理驼峰参数转换为下划线
+ // if dbField, ok := camelToSnakeMap[key]; ok {
+ // key = dbField
+ // }
+ //
+ // // 特殊处理book_pic参数
+ // if key == "book_pic" {
+ // if value == "0" {
+ // query += " AND (book_pic IS NULL OR book_pic = '')"
+ // } else if value == "1" {
+ // query += " AND (book_pic IS NOT NULL AND book_pic != '')"
+ // }
+ // continue
+ // }
+ //
+ // // 特殊处理category参数
+ // if key == "category" {
+ // // 对URL编码的值进行解码
+ // decodedValue, err := url.QueryUnescape(value)
+ // if err != nil {
+ // decodedValue = value
+ // }
+ // // 使用LIKE进行模糊匹配
+ // query += " AND category LIKE ?"
+ // args = append(args, "%"+decodedValue+"%")
+ // continue
+ // }
+ //
+ // // 特殊处理buy_count参数 - 根据saleSelect使用不同的字段
+ // if key == "buy_counts" {
+ // key = saleCountField
+ // }
+ //
+ // if contains(exactFields, key) {
+ // query += fmt.Sprintf(" AND %s = ?", key)
+ // args = append(args, value)
+ // continue
+ // }
+ //
+ // if contains(numericRangeFields, key) {
+ // if strings.Contains(value, ",") {
+ // parts := strings.Split(value, ",")
+ // if len(parts) == 2 {
+ // query += fmt.Sprintf(" AND %s BETWEEN ? AND ?", key)
+ // args = append(args, parts[0], parts[1])
+ // }
+ // } else {
+ // query += fmt.Sprintf(" AND %s > ?", key)
+ // args = append(args, value)
+ // }
+ // continue
+ // }
+ //
+ // if contains(boolFields, key) {
+ // if value == "0" {
+ // query += fmt.Sprintf(" AND %s = 0", key)
+ // } else if value == "1" {
+ // query += fmt.Sprintf(" AND %s = 1", key)
+ // }
+ // continue
+ // }
+ //
+ // if contains(dateFields, key) {
+ // if strings.Contains(value, ",") {
+ // parts := strings.Split(value, ",")
+ // if len(parts) == 2 {
+ // query += fmt.Sprintf(" AND %s BETWEEN ? AND ?", key)
+ // args = append(args, parts[0], parts[1])
+ // }
+ // } else {
+ // query += fmt.Sprintf(" AND %s >= ? AND %s < DATE_ADD(?, INTERVAL 1 DAY)", key, key)
+ // args = append(args, value, value)
+ // }
+ // continue
+ // }
+ // //// 特殊处理书名和作者的全模糊查询
+ // //if key == "book_name" || key == "author" {
+ // // query += fmt.Sprintf(" AND %s LIKE ?", key)
+ // // args = append(args, "%"+value+"%")
+ // // continue
+ // //}
+ // if key == "book_name" || key == "author" {
+ // query += fmt.Sprintf(" AND MATCH(%s) AGAINST(? IN BOOLEAN MODE)", key)
+ // args = append(args, value)
+ // continue
+ // }
+ // // 对于已知前缀的查询使用索引
+ // if strings.HasPrefix(value, "%") {
+ // query += fmt.Sprintf(" AND %s LIKE ?", key)
+ // } else {
+ // query += fmt.Sprintf(" AND %s LIKE ?", key)
+ // value = value + "%"
+ // }
+ // args = append(args, value)
+ //}
+ //
+ //// 获取总数
+ //countQuery := "SELECT COUNT(*) FROM (" + query + ") AS count_table"
+ //var total int
+ //if err := bc.db.QueryRow(countQuery, args...).Scan(&total); err != nil {
+ // log.Printf("Count query error: %v", err)
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "获取总数失败"})
+ // return
+ //}
+ //
+ //// 处理分页参数
+ //perPage, err := getIntParam(c, "per_page", "pageSize", 10)
+ //if err != nil {
+ // c.JSON(http.StatusBadRequest, gin.H{"error": "每页数量参数无效"})
+ // return
+ //}
+ //
+ //page, err := getInt64Param(c, "page", "pageNum", 1)
+ //if err != nil {
+ // c.JSON(http.StatusBadRequest, gin.H{"error": "页码参数无效"})
+ // return
+ //}
+ //
+ //var results []map[string]interface{}
+ //
+ //// 优化后的随机查询逻辑
+ //randomQuery := query + " ORDER BY RAND() LIMIT ?"
+ //randomArgs := append(args, perPage)
+ //
+ //// 打印SQL查询语句和参数用于调试
+ ////log.Printf("GetRandomBookBaseInfo SQL查询: %s", randomQuery)
+ ////log.Printf("GetRandomBookBaseInfo 查询参数: %+v", randomArgs)
+ //
+ //stmt, err := bc.db.Prepare(randomQuery)
+ //if err != nil {
+ // log.Printf("Prepare statement error: %v", err)
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "准备查询语句失败"})
+ // return
+ //}
+ //defer stmt.Close()
+ //
+ //rows, err := stmt.Query(randomArgs...)
+ //if err != nil {
+ // log.Printf("Database query error: %v", err)
+ // c.JSON(http.StatusInternalServerError, gin.H{
+ // "error": "数据库查询失败",
+ // "details": err.Error(),
+ // "query": randomQuery,
+ // })
+ // return
+ //}
+ //defer rows.Close()
+ //
+ //// 处理查询结果
+ //columns, err := rows.Columns()
+ //if err != nil {
+ // log.Printf("Get columns error: %v", err)
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "获取列信息失败"})
+ // return
+ //}
+ //
+ //for rows.Next() {
+ // values := make([]interface{}, len(columns))
+ // pointers := make([]interface{}, len(columns))
+ // for i := range values {
+ // pointers[i] = &values[i]
+ // }
+ //
+ // if err := rows.Scan(pointers...); err != nil {
+ // log.Printf("Row scan error: %v", err)
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "读取行数据失败"})
+ // return
+ // }
+ //
+ // row := make(map[string]interface{})
+ //
+ // for i, col := range columns {
+ // if b, ok := values[i].([]byte); ok {
+ // row[col] = string(b)
+ // } else {
+ // row[col] = values[i]
+ // }
+ // }
+ //
+ // results = append(results, row)
+ //}
+ //
+ //if err := rows.Err(); err != nil {
+ // log.Printf("Rows error: %v", err)
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "处理结果集时出错"})
+ // return
+ //}
+ //
+ //// 构建响应数据
+ //responseData := gin.H{
+ // "data": results,
+ // "total": total,
+ // "per_page": perPage,
+ // "current_page": page,
+ //}
+ //
+ //// 记录查询时间
+ //elapsed := time.Since(startTime)
+ //log.Printf("Query executed in %v", elapsed)
+ //
+ //c.JSON(http.StatusOK, responseData)
+}
+
+// GetBookByISBN 根据isbn查询(判断是否合法,在查询Redis,没有在调用博查询详情数据的接口)
+func (bc *BookCenterController) GetBookByISBN(c *gin.Context) {
+ // 获取ISBN参数
+ isbn := c.Query("isbn")
+ if isbn == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN不能为空"})
+ return
+ }
+
+ // 验证ISBN格式
+ if !isValidISBN(isbn) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN格式不正确"})
+ return
+ }
+
+ // 1. 先尝试从Redis获取
+ redisKey := isbn
+ bookData, err := bc.redis.Get(context.Background(), redisKey).Result()
+ if err == nil {
+ var result map[string]interface{}
+ if err := json.Unmarshal([]byte(bookData), &result); err == nil {
+ bookPicSMissing := false
+ bookPicNewMissing := false
+ publisherMissing := false
+ publicationTimeMissing := false
+
+ // 检查图片字段
+ if bookPicS, exists := result["book_pic_s"]; !exists || bookPicS == nil || bookPicS == "" {
+ bookPicSMissing = true
+ }
+ if bookPicNew, exists := result["book_pic_new"]; !exists || bookPicNew == nil || bookPicNew == "" {
+ bookPicNewMissing = true
+ }
+
+ // 检查 publisher 是否为空
+ if publisher, exists := result["publisher"]; !exists || publisher == nil || publisher == "" {
+ publisherMissing = true
+ }
+
+ // 检查 publication_time 是否为空或为 0
+ if pubTime, exists := result["publication_time"]; !exists || pubTime == nil {
+ publicationTimeMissing = true
+ } else {
+ switch v := pubTime.(type) {
+ case float64:
+ if v == 0 {
+ publicationTimeMissing = true
+ }
+ case int, int64:
+ if reflect.ValueOf(v).Int() == 0 {
+ publicationTimeMissing = true
+ }
+ case string:
+ if v == "" || v == "0" {
+ publicationTimeMissing = true
+ }
+ }
+ }
+
+ // ===== 如果 publisher 或 publication_time 缺失,从外部API补充 =====
+ if publisherMissing || publicationTimeMissing {
+ log.Printf("Redis中ISBN %s的publisher或publication_time字段缺失,调用外部API补充", isbn)
+
+ apiUrl := detailPagesUrl + "/api/image-url?isbn=" + isbn
+ log.Printf("调用外部API: %s", apiUrl)
+
+ resp, err := http.Get(apiUrl)
+ if err == nil && resp.StatusCode == http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+
+ var apiResponse struct {
+ Success bool `json:"success"`
+ Data struct {
+ Detail struct {
+ Publisher string `json:"publisher"`
+ PublicationTime FlexInt64 `json:"publication_time"`
+ } `json:"detail"`
+ } `json:"data"`
+ }
+
+ if err := json.Unmarshal(bodyBytes, &apiResponse); err == nil && apiResponse.Success {
+ if publisherMissing && apiResponse.Data.Detail.Publisher != "" {
+ result["publisher"] = apiResponse.Data.Detail.Publisher
+ log.Printf("外部API补充publisher字段: %s", apiResponse.Data.Detail.Publisher)
+ }
+ if publicationTimeMissing && apiResponse.Data.Detail.PublicationTime > 0 {
+ result["publication_time"] = apiResponse.Data.Detail.PublicationTime
+ log.Printf("外部API补充publication_time字段: %v", apiResponse.Data.Detail.PublicationTime)
+ }
+
+ // 更新 Redis
+ if resultJSON, err := json.Marshal(result); err == nil {
+ if err := bc.redis.Set(context.Background(), redisKey, resultJSON, 0).Err(); err != nil {
+ log.Printf("更新Redis失败: %v", err)
+ } else {
+ log.Printf("成功更新Redis中ISBN %s的publisher/publication_time字段", isbn)
+ }
+ }
+
+ // 更新数据库
+ query := `UPDATE book_center SET publisher = ?, publication_time = ? WHERE isbn = ?`
+ _, err = bc.db.Exec(query,
+ result["publisher"],
+ result["publication_time"],
+ isbn,
+ )
+ if err != nil {
+ log.Printf("更新数据库publisher/publication_time失败: %v", err)
+ } else {
+ log.Printf("成功更新数据库中ISBN %s的publisher/publication_time字段", isbn)
+ }
+ } else {
+ log.Printf("外部API调用或解析失败: %v", err)
+ }
+ } else {
+ log.Printf("外部API调用失败或返回状态码错误: %v, 状态码: %d", err, resp.StatusCode)
+ }
+ }
+
+ // ===== 图片字段缺失处理(保持你原有逻辑) =====
+ if bookPicSMissing || bookPicNewMissing {
+ log.Printf("Redis中ISBN %s的book_pic_s或book_pic_new字段缺失,从数据库查询补充", isbn)
+ query := `SELECT book_pic_s, book_pic_new FROM book_center WHERE isbn = ?`
+ var dbBookPicS, dbBookPicNew sql.NullString
+
+ err := bc.db.QueryRow(query, isbn).Scan(&dbBookPicS, &dbBookPicNew)
+ if err == nil {
+ if bookPicSMissing && dbBookPicS.Valid && dbBookPicS.String != "" {
+ result["book_pic_s"] = dbBookPicS.String
+ } else if bookPicSMissing {
+ result["book_pic_s"] = ""
+ }
+
+ if bookPicNewMissing && dbBookPicNew.Valid && dbBookPicNew.String != "" {
+ result["book_pic_new"] = dbBookPicNew.String
+ } else if bookPicNewMissing {
+ result["book_pic_new"] = ""
+ }
+
+ // 更新Redis缓存
+ if resultJSON, err := json.Marshal(result); err == nil {
+ if err := bc.redis.Set(context.Background(), redisKey, resultJSON, 0).Err(); err != nil {
+ log.Printf("更新Redis缓存失败: %v", err)
+ }
+ }
+ }
+ }
+
+ // 调用 UploadBookImage 方法
+ bc.processBookImageUpload(result, isbn)
+
+ c.JSON(http.StatusOK, gin.H{"data": result, "source": "redis"})
+ return
+ }
+ }
+
+ // 2. Redis中没有,尝试从数据库获取
+ log.Printf("Redis中未找到ISBN %s,尝试从数据库查询", isbn)
+ query := `SELECT
+ id, book_name, book_pic, book_pic_s, book_pic_new, isbn, author, category, publisher,
+ publication_time, binding_layout, fix_price, buy_counts, sell_counts,
+ vio_book, book_set, onenum_mbooks, ill_publisher, ill_author,
+ day_sale_7, day_sale_15, day_sale_30, day_sale_60, day_sale_90,
+ day_sale_180, day_sale_365, this_year_sale, last_year_sale, total_sale,
+ update_time
+ FROM book_center
+ WHERE isbn = ?`
+
+ var (
+ id, bookName, bookPic, dbIsbn, author, category, publisher string
+ bindingLayout sql.NullString
+ bookPicS, bookPicNew sql.NullString
+ buyCount, sellCount sql.NullString
+ publicationTime interface{}
+ fixPrice int64
+ vioBook, bookSet, onenumMbooks, illPublisher, illAuthor int
+ daySale7, daySale15, daySale30, daySale60, daySale90 int
+ daySale180, daySale365, thisYearSale, lastYearSale, totalSale int
+ updateTime interface{}
+ )
+
+ err = bc.db.QueryRow(query, isbn).Scan(
+ &id, &bookName, &bookPic, &bookPicS, &bookPicNew, &dbIsbn, &author, &category, &publisher,
+ &publicationTime, &bindingLayout, &fixPrice, &buyCount, &sellCount,
+ &vioBook, &bookSet, &onenumMbooks, &illPublisher, &illAuthor,
+ &daySale7, &daySale15, &daySale30, &daySale60, &daySale90,
+ &daySale180, &daySale365, &thisYearSale, &lastYearSale, &totalSale,
+ &updateTime)
+
+ if err == nil {
+ // 数据库中找到数据,构建返回结果
+ log.Printf("数据库中找到ISBN %s的数据", isbn)
+
+ // 处理book_pic_s字段
+ bookPicSValue := ""
+ if bookPicS.Valid && bookPicS.String != "" {
+ bookPicSValue = bookPicS.String
+ }
+
+ // 处理book_pic_new字段
+ bookPicNewValue := ""
+ if bookPicNew.Valid && bookPicNew.String != "" {
+ bookPicNewValue = bookPicNew.String
+ }
+
+ // 处理binding_layout字段
+ bindingLayoutValue := ""
+ if bindingLayout.Valid && bindingLayout.String != "" {
+ bindingLayoutValue = bindingLayout.String
+ }
+
+ // 处理buy_counts字段
+ buyCountValue := ""
+ if buyCount.Valid && buyCount.String != "" {
+ buyCountValue = buyCount.String
+ }
+
+ // 处理sell_counts字段
+ sellCountValue := ""
+ if sellCount.Valid && sellCount.String != "" {
+ sellCountValue = sellCount.String
+ }
+
+ result := map[string]interface{}{
+ "id": id,
+ "book_name": bookName,
+ "book_pic": bookPic,
+ "book_pic_s": bookPicSValue,
+ "book_pic_new": bookPicNewValue,
+ "isbn": dbIsbn,
+ "author": author,
+ "category": category,
+ "publisher": publisher,
+ "publication_time": publicationTime,
+ "binding_layout": bindingLayoutValue,
+ "fix_price": fixPrice,
+ "buy_counts": buyCountValue,
+ "sell_counts": sellCountValue,
+ "vio_book": vioBook,
+ "book_set": bookSet,
+ "onenum_mbooks": onenumMbooks,
+ "ill_publisher": illPublisher,
+ "ill_author": illAuthor,
+ "day_sale_7": daySale7,
+ "day_sale_15": daySale15,
+ "day_sale_30": daySale30,
+ "day_sale_60": daySale60,
+ "day_sale_90": daySale90,
+ "day_sale_180": daySale180,
+ "day_sale_365": daySale365,
+ "this_year_sale": thisYearSale,
+ "last_year_sale": lastYearSale,
+ "total_sale": totalSale,
+ "update_time": updateTime,
+ }
+
+ // 调用 UploadBookImage 方法并更新字段
+ bc.processBookImageUpload(result, isbn)
+
+ // 将数据库查询结果同步到Redis缓存
+ if resultJSON, err := json.Marshal(result); err == nil {
+ if err := bc.redis.Set(context.Background(), redisKey, resultJSON, 0).Err(); err != nil {
+ log.Printf("同步数据到Redis失败: %v", err)
+ } else {
+ log.Printf("成功将ISBN %s的数据同步到Redis", isbn)
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{"data": result, "source": "database"})
+ return
+ } else if err != sql.ErrNoRows {
+ // 数据库查询出错(非记录不存在)
+ log.Printf("数据库查询失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败"})
+ return
+ }
+
+ // 3. Redis和数据库中都没有,调用外部API
+ log.Printf("Redis和数据库中都未找到ISBN %s,调用外部API", isbn)
+ //apiUrl := "http://175.27.224.66:8091/api/image-url?isbn=" + isbn
+ apiUrl := detailPagesUrl + "/api/image-url?isbn=" + isbn
+
+ // 打印请求URL
+ log.Printf("调用外部API: %s", apiUrl)
+
+ resp, err := http.Get(apiUrl)
+ if err != nil {
+ log.Printf("调用外部API失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "调用外部API失败"})
+ return
+ }
+ defer resp.Body.Close()
+
+ // 打印响应状态码
+ log.Printf("外部API响应状态码: %d", resp.StatusCode)
+
+ if resp.StatusCode != http.StatusOK {
+ log.Printf("外部API返回错误状态码: %d", resp.StatusCode)
+ c.JSON(http.StatusNotFound, gin.H{"error": "未找到该ISBN对应的图书"})
+ return
+ }
+
+ // 读取响应体
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Printf("读取API响应体失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "读取API响应失败"})
+ return
+ }
+
+ // 打印原始响应数据
+ log.Printf("外部API原始响应数据: %s", string(bodyBytes))
+
+ // 解析API响应
+ var apiResponse struct {
+ Success bool `json:"success"`
+ Data struct {
+ Detail struct {
+ Editor string `json:"editor"`
+ Languages string `json:"languages"`
+ PrintTime string `json:"print_time"`
+ ISBN string `json:"isbn"`
+ BookPic string `json:"book_pic"`
+ BookName string `json:"book_name"`
+ Author string `json:"author"`
+ Format string `json:"format"`
+ Publisher string `json:"publisher"`
+ Paper string `json:"paper"`
+ PublicationTime FlexInt64 `json:"publication_time"`
+ Pages string `json:"pages"`
+ Edition string `json:"edition"`
+ Wordage string `json:"wordage"`
+ Category string `json:"category"`
+ FixPrice string `json:"fix_price"`
+ BindingLayout string `json:"binding_layout"`
+ BuyCount string `json:"buyCount"`
+ SellCount string `json:"sellCount"`
+ Content string `json:"content"`
+ } `json:"detail"`
+ ImageURL string `json:"image_url"`
+ } `json:"data"`
+ }
+
+ // 使用已读取的bodyBytes进行解析
+ if err := json.Unmarshal(bodyBytes, &apiResponse); err != nil {
+ log.Printf("解析API响应失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "解析API响应失败"})
+ return
+ }
+
+ // 打印解析后的API响应结构
+ log.Printf("解析后的API响应: %+v", apiResponse)
+ log.Printf("外部API返回的BindingLayout值: '%s'", apiResponse.Data.Detail.BindingLayout)
+
+ // 转换数据格式
+ transformedData := map[string]interface{}{
+ "author": apiResponse.Data.Detail.Author,
+ "binding_layout": apiResponse.Data.Detail.BindingLayout,
+ "book_name": apiResponse.Data.Detail.BookName,
+ "book_pic": apiResponse.Data.ImageURL,
+ "book_pic_s": nil,
+ "book_set": 0,
+ "buy_count": apiResponse.Data.Detail.BuyCount,
+ "buy_counts": parseCount(apiResponse.Data.Detail.BuyCount),
+ "cat_id": 0,
+ "category": apiResponse.Data.Detail.Category,
+ "content": apiResponse.Data.Detail.Content,
+ "create_by": 1,
+ "create_time": time.Now().Unix(),
+ "day_sale_15": 0,
+ "day_sale_180": 0,
+ "day_sale_30": 0,
+ "day_sale_365": 0,
+ "day_sale_60": 0,
+ "day_sale_7": 0,
+ "day_sale_90": 0,
+ "edition": apiResponse.Data.Detail.Edition,
+ "editor": apiResponse.Data.Detail.Editor,
+ "fix_price": parsePrice(apiResponse.Data.Detail.FixPrice),
+ "format": apiResponse.Data.Detail.Format,
+ "id": 0,
+ "ill_author": 0,
+ "ill_publisher": 0,
+ "isbn": getISBNValue(apiResponse.Data.Detail.ISBN, isbn),
+ "languages": apiResponse.Data.Detail.Languages,
+ "last_year_sale": 0,
+ "onenum_mbooks": 0,
+ "pages": parseCount(apiResponse.Data.Detail.Pages),
+ "paper": apiResponse.Data.Detail.Paper,
+ "print_time": parseTime(apiResponse.Data.Detail.PrintTime),
+ "publication_time": apiResponse.Data.Detail.PublicationTime,
+ "publiction_times": apiResponse.Data.Detail.PublicationTime,
+ "publisher": apiResponse.Data.Detail.Publisher,
+ "remark": "",
+ "sell_count": apiResponse.Data.Detail.SellCount,
+ "sell_counts": parseCount(apiResponse.Data.Detail.SellCount),
+ "shipment_cycle": 0,
+ "sold_out_times": "[]",
+ "this_year_sale": 0,
+ "total_sale": 0,
+ "update_by": 0,
+ "update_time": time.Now().Unix(),
+ "vio_book": 0,
+ "wordage": apiResponse.Data.Detail.Wordage,
+ }
+ // 添加详细的调试日志,检查transformedData中的binding_layout字段
+ log.Printf("=== GetBookByISBN Debug Info ===")
+ log.Printf("ISBN: %s", isbn)
+ log.Printf("外部API返回的BindingLayout原始值: '%s'", apiResponse.Data.Detail.BindingLayout)
+ log.Printf("transformedData中的binding_layout值: '%v'", transformedData["binding_layout"])
+ log.Printf("transformedData中binding_layout字段类型: %T", transformedData["binding_layout"])
+
+ // 检查transformedData中是否包含binding_layout字段
+ if val, exists := transformedData["binding_layout"]; exists {
+ log.Printf("✅ binding_layout字段存在于transformedData中,值为: '%v'", val)
+ } else {
+ log.Printf("❌ binding_layout字段不存在于transformedData中")
+ }
+
+ // 打印transformedData的所有键
+ keys := make([]string, 0, len(transformedData))
+ for k := range transformedData {
+ keys = append(keys, k)
+ }
+ log.Printf("transformedData包含的所有字段: %v", keys)
+ log.Printf("=== End Debug Info ===")
+
+ // 3. 异步存储到Redis和MySQL
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Printf("异步存储过程发生panic: %v, ISBN: %s", r, isbn)
+ }
+ }()
+
+ // 存储到Redis - 将数据转换为JSON字符串
+ jsonData, err := json.Marshal(transformedData)
+ if err != nil {
+ log.Printf("序列化数据失败: %v, ISBN: %s", err, isbn)
+ return
+ }
+ if err := bc.redis.Set(context.Background(), redisKey, jsonData, 0).Err(); err != nil {
+ log.Printf("存储到Redis失败: %v, ISBN: %s", err, isbn)
+ }
+
+ // 存储到中心书库(MySQL)
+ if err := bc.saveToBookCenter(transformedData); err != nil {
+ log.Printf("存储到中心书库失败: %v, ISBN: %s", err, isbn)
+ }
+ }()
+
+ // 最终响应前的调试日志
+ log.Printf("=== 准备返回响应 ===")
+ log.Printf("ISBN: %s", isbn)
+
+ // 检查最终响应数据中的binding_layout字段
+ if val, exists := transformedData["binding_layout"]; exists {
+ log.Printf("✅ 最终响应数据包含binding_layout字段,值为: '%v'", val)
+ } else {
+ log.Printf("❌ 最终响应数据不包含binding_layout字段")
+ }
+
+ // 序列化响应数据用于日志记录
+ responseJSON, err := json.Marshal(transformedData)
+ if err != nil {
+ log.Printf("序列化响应数据失败: %v", err)
+ } else {
+ log.Printf("完整响应数据JSON: %s", string(responseJSON))
+ }
+ log.Printf("=== 响应发送完成 ===")
+
+ // 调用 UploadBookImage 方法并更新字段
+ bc.processBookImageUpload(transformedData, isbn)
+
+ c.JSON(http.StatusOK, gin.H{"data": transformedData, "source": "external_api"})
+
+}
+
+// 存储到中心书库(MySQL)
+func (bc *BookCenterController) saveToBookCenter(data map[string]interface{}) error {
+ // 检查数据库连接
+ if err := bc.db.Ping(); err != nil {
+ log.Printf("saveToBookCenter 数据库连接失败: %v", err)
+ return fmt.Errorf("数据库连接失败: %w", err)
+ }
+
+ // 先检查ISBN是否已存在
+ var existingID int
+ checkQuery := "SELECT id FROM book_center WHERE isbn = ? LIMIT 1"
+ err := bc.db.QueryRow(checkQuery, data["isbn"]).Scan(&existingID)
+ if err == nil {
+ return nil // ISBN已存在,不需要插入
+ } else if err != sql.ErrNoRows {
+ log.Printf("saveToBookCenter 检查ISBN是否存在时出错: %v", err)
+ return fmt.Errorf("检查ISBN是否存在时出错: %w", err)
+ }
+
+ // 生成ISBN的MD5哈希值
+ isbnHash := generateISBNHash(data["isbn"].(string))
+
+ // 处理JSON字段 - 将map转换为JSON字符串
+ var bookPicS, bookPicNew interface{}
+
+ // 处理book_pic_s字段
+ if val, exists := data["book_pic_s"]; exists && val != nil {
+ if mapVal, ok := val.(map[string]interface{}); ok {
+ if jsonBytes, err := json.Marshal(mapVal); err == nil {
+ bookPicS = string(jsonBytes)
+ log.Printf("saveToBookCenter 转换book_pic_s为JSON: %s", string(jsonBytes))
+ } else {
+ log.Printf("saveToBookCenter book_pic_s JSON序列化失败: %v", err)
+ bookPicS = nil
+ }
+ } else {
+ bookPicS = val
+ }
+ } else {
+ bookPicS = nil
+ }
+
+ // 处理book_pic_new字段
+ if val, exists := data["book_pic_new"]; exists && val != nil {
+ if mapVal, ok := val.(map[string]interface{}); ok {
+ if jsonBytes, err := json.Marshal(mapVal); err == nil {
+ bookPicNew = string(jsonBytes)
+ log.Printf("saveToBookCenter 转换book_pic_new为JSON: %s", string(jsonBytes))
+ } else {
+ log.Printf("saveToBookCenter book_pic_new JSON序列化失败: %v", err)
+ bookPicNew = nil
+ }
+ } else {
+ bookPicNew = val
+ }
+ } else {
+ bookPicNew = nil
+ }
+
+ query := `INSERT INTO book_center (
+ category, book_name, book_pic, book_pic_s, book_pic_new, isbn, isbn_hash, author,
+ editor, binding_layout, publisher, edition, format, languages,
+ publication_time, print_time, paper, pages, wordage, fix_price,
+ content, remark, vio_book, book_set, onenum_mbooks, ill_publisher,
+ ill_author, day_sale_7, day_sale_15, day_sale_30, day_sale_60,
+ day_sale_90, day_sale_180, day_sale_365, this_year_sale, last_year_sale,
+ total_sale, sold_out_times, shipment_cycle, cat_id, buy_counts, sell_counts, del_flag,
+ create_by, create_time, update_by, update_time
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+
+ result, err := bc.db.Exec(query,
+ data["category"],
+ data["book_name"],
+ data["book_pic"],
+ bookPicS,
+ bookPicNew,
+ data["isbn"],
+ isbnHash, // isbn_hash
+ data["author"],
+ data["editor"],
+ data["binding_layout"],
+ data["publisher"],
+ data["edition"],
+ data["format"],
+ data["languages"],
+ data["publication_time"], // 注意拼写
+ data["print_time"],
+ data["paper"],
+ data["pages"],
+ data["wordage"],
+ data["fix_price"],
+ data["content"],
+ data["remark"],
+ data["vio_book"],
+ data["book_set"],
+ data["onenum_mbooks"],
+ data["ill_publisher"],
+ data["ill_author"],
+ data["day_sale_7"],
+ data["day_sale_15"],
+ data["day_sale_30"],
+ data["day_sale_60"],
+ data["day_sale_90"],
+ data["day_sale_180"],
+ data["day_sale_365"],
+ data["this_year_sale"],
+ data["last_year_sale"],
+ data["total_sale"],
+ data["sold_out_times"],
+ data["shipment_cycle"],
+ data["cat_id"],
+ data["buy_counts"],
+ data["sell_counts"],
+ 0, // del_flag
+ 1, // create_by
+ time.Now().Unix(), // create_time
+ 1, // update_by
+ time.Now().Unix(), // update_time
+ )
+
+ if err != nil {
+ log.Printf("saveToBookCenter 插入书籍失败: %v, ISBN: %v", err, data["isbn"])
+ log.Printf("saveToBookCenter 失败的完整数据: %+v", data)
+ return fmt.Errorf("插入书籍失败: %w", err)
+ }
+
+ // 获取插入的行ID
+ lastInsertID, err := result.LastInsertId()
+ if err != nil {
+ log.Printf("saveToBookCenter 获取插入ID失败: %v", err)
+ } else {
+ log.Printf("saveToBookCenter 成功插入书籍,ID: %d, ISBN: %v", lastInsertID, data["isbn"])
+ }
+
+ // 验证插入是否成功
+ var count int
+ countQuery := "SELECT COUNT(*) FROM book_center WHERE isbn = ?"
+ if err := bc.db.QueryRow(countQuery, data["isbn"]).Scan(&count); err != nil {
+ log.Printf("saveToBookCenter 验证插入失败: %v", err)
+ } else {
+ log.Printf("saveToBookCenter 验证结果: ISBN %v 在数据库中的记录数: %d", data["isbn"], count)
+ }
+
+ return nil
+}
+
+// 辅助函数:解析数量
+func parseCount(countStr string) int {
+ if countStr == "" {
+ return 0
+ }
+ count, err := strconv.Atoi(countStr)
+ if err != nil {
+ return 0
+ }
+ return count
+}
+
+// 辅助函数:解析价格(单位:分)
+func parsePrice(priceStr string) int {
+ if priceStr == "" {
+ return 0
+ }
+ price, err := strconv.ParseFloat(priceStr, 64)
+ if err != nil {
+ return 0
+ }
+ return int(price * 100)
+}
+
+// 辅助函数:解析时间
+func parseTime(timeStr string) int64 {
+ if timeStr == "" {
+ return 0
+ }
+ // 这里可以根据实际时间格式进行解析
+ // 示例中简单返回0
+ return 0
+}
+
+// ISBN验证函数
+func isValidISBN(isbn string) bool {
+ // 移除所有非数字和X字符
+ cleaned := strings.ReplaceAll(strings.ToUpper(isbn), "-", "")
+ cleaned = strings.ReplaceAll(cleaned, " ", "")
+
+ // ISBN-10或ISBN-13验证
+ if len(cleaned) == 10 {
+ return isValidISBN10(cleaned)
+ } else if len(cleaned) == 13 {
+ return isValidISBN13(cleaned)
+ }
+ return false
+}
+
+// 检验isbn(10位)
+func isValidISBN10(s string) bool {
+ // 检查长度是否为10
+ if len(s) != 10 {
+ return false
+ }
+
+ // 检查前9位是否全部为数字
+ if _, err := strconv.Atoi(s[:9]); err != nil {
+ return false
+ }
+
+ // 检查最后一位是否为数字或'X'
+ lastChar := s[9]
+ if !(lastChar >= '0' && lastChar <= '9') && lastChar != 'X' {
+ return false
+ }
+
+ // 计算校验和
+ sum := 0
+ for i := 0; i < 9; i++ {
+ digit, _ := strconv.Atoi(string(s[i]))
+ sum += digit * (10 - i)
+ }
+
+ // 处理校验位
+ var checkDigit int
+ if lastChar == 'X' {
+ checkDigit = 10
+ } else {
+ checkDigit, _ = strconv.Atoi(string(lastChar))
+ }
+
+ return (sum+checkDigit)%11 == 0
+}
+
+// 校验isbn(13位)
+func isValidISBN13(s string) bool {
+ // 检查长度是否为13
+ if len(s) != 13 {
+ return false
+ }
+
+ // 检查是否全部为数字
+ if _, err := strconv.Atoi(s); err != nil {
+ return false
+ }
+
+ // 计算校验和
+ sum := 0
+ for i := 0; i < 12; i++ {
+ digit, _ := strconv.Atoi(string(s[i]))
+ if i%2 == 0 { // 奇数位(从0开始计数)
+ sum += digit
+ } else { // 偶数位
+ sum += digit * 3
+ }
+ }
+
+ // 计算校验位
+ checkDigit, _ := strconv.Atoi(string(s[12]))
+ calculatedCheck := (10 - (sum % 10)) % 10
+
+ return checkDigit == calculatedCheck
+}
+
+// BookInfo 构建Redis中数据结构体
+type BookInfo struct {
+ ID FlexInt `json:"id"`
+ Author string `json:"author"`
+ BindingLayout string `json:"binding_layout"`
+ BookName string `json:"book_name"`
+ BookPic string `json:"book_pic"`
+ BookPicS BookPicS `json:"book_pic_s"`
+ BookPicNew BookPicS `json:"book_pic_new"` // 如果可能也是混合类型 BookSet FlexInt `json:"book_set"`
+ BookSet FlexInt `json:"book_set"`
+ BuyCount string `json:"buy_count"`
+ DaySale7 FlexInt `json:"day_sale_7"`
+ DaySale15 FlexInt `json:"day_sale_15"`
+ DaySale30 FlexInt `json:"day_sale_30"`
+ DaySale60 FlexInt `json:"day_sale_60"`
+ DaySale90 FlexInt `json:"day_sale_90"`
+ DaySale180 FlexInt `json:"day_sale_180"`
+ DaySale365 FlexInt `json:"day_sale_365"`
+ FixPrice FlexInt64 `json:"fix_price"`
+ IllAuthor FlexInt `json:"ill_author"`
+ IllPublisher FlexInt `json:"ill_publisher"`
+ Isbn string `json:"isbn"`
+ LastYearSale FlexInt `json:"last_year_sale"`
+ OnenumMbooks FlexInt `json:"onenum_mbooks"`
+ PublicationTime FlexInt64 `json:"publication_time"`
+ Publisher string `json:"publisher"`
+ SellCount string `json:"sell_count"`
+ ThisYearSale FlexInt `json:"this_year_sale"`
+ TotalSale FlexInt `json:"total_sale"`
+ VioBook FlexInt `json:"vio_book"`
+}
+
+// SetBookBaseInfoToIllRequest 定义请求参数结构
+type SetBookBaseInfoToIllRequest struct {
+ IDs interface{} `json:"ids"` // 可以是字符串或数组
+ VioBook *int `json:"vio_book"` // 使用指针表示可选字段
+ BookSet *int `json:"book_set"` // 使用指针表示可选字段
+ OnenumMbooks *int `json:"onenum_mbooks"` // 使用指针表示可选字段
+ IllPublisher *int `json:"ill_publisher"` // 使用指针表示可选字段
+ IllAuthor *int `json:"ill_author"` // 使用指针表示可选字段
+}
+
+// SetBookBaseInfoToIllByISBNRequest 根据ISBN修改违规信息的请求参数结构
+type SetBookBaseInfoToIllByISBNRequest struct {
+ ISBNs interface{} `json:"isbns"` // 可以是字符串或数组
+ VioBook *int `json:"vio_book"` // 使用指针表示可选字段
+ BookSet *int `json:"book_set"` // 使用指针表示可选字段
+ OnenumMbooks *int `json:"onenum_mbooks"` // 使用指针表示可选字段
+ IllPublisher *int `json:"ill_publisher"` // 使用指针表示可选字段
+ IllAuthor *int `json:"ill_author"` // 使用指针表示可选字段
+}
+
+// FlexInt64 灵活的int64类型,可以接受字符串或数字
+type FlexInt64 int64
+
+// UnmarshalJSON 自定义JSON解析,支持字符串和数字
+func (fi *FlexInt64) UnmarshalJSON(data []byte) error {
+ // 去除引号
+ str := strings.Trim(string(data), `"`)
+
+ // 如果是空字符串或null,设为0
+ if str == "" || str == "null" {
+ *fi = 0
+ return nil
+ }
+
+ // 处理日期格式
+ if strings.Contains(str, "-") {
+ // 尝试解析日期格式
+ formats := []string{"2006-01", "2006-01-02", "2006"}
+ for _, format := range formats {
+ if t, err := time.Parse(format, str); err == nil {
+ *fi = FlexInt64(t.Unix())
+ return nil
+ }
+ }
+ }
+
+ // 尝试解析为整数
+ val, err := strconv.ParseInt(str, 10, 64)
+ if err != nil {
+ *fi = 0 // 解析失败时设为0
+ return nil
+ }
+
+ *fi = FlexInt64(val)
+ return nil
+}
+
+// MarshalJSON 自定义JSON序列化
+func (fi FlexInt64) MarshalJSON() ([]byte, error) {
+ return []byte(strconv.FormatInt(int64(fi), 10)), nil
+}
+
+// FlexInt 灵活的int类型,可以接受字符串或数字
+type FlexInt int
+
+// UnmarshalJSON 自定义JSON解析,支持字符串和数字
+func (fi *FlexInt) UnmarshalJSON(data []byte) error {
+ // 去除引号
+ str := strings.Trim(string(data), `"`)
+
+ // 如果是空字符串或null,设为0
+ if str == "" || str == "null" {
+ *fi = 0
+ return nil
+ }
+
+ // 尝试解析为整数
+ val, err := strconv.Atoi(str)
+ if err != nil {
+ *fi = 0 // 解析失败时设为0
+ return nil
+ }
+
+ *fi = FlexInt(val)
+ return nil
+}
+
+// MarshalJSON 自定义JSON序列化
+func (fi FlexInt) MarshalJSON() ([]byte, error) {
+ return []byte(strconv.Itoa(int(fi))), nil
+}
+
+// FlexNumber 灵活的数字类型,可以接受字符串或数字
+type FlexNumber struct {
+ Value int
+}
+
+// UnmarshalJSON 自定义JSON解析,支持字符串和数字
+func (fn *FlexNumber) UnmarshalJSON(data []byte) error {
+ // 去除引号
+ str := strings.Trim(string(data), `"`)
+
+ // 如果是空字符串,设为0
+ if str == "" || str == "null" {
+ fn.Value = 0
+ return nil
+ }
+
+ // 尝试解析为整数
+ val, err := strconv.Atoi(str)
+ if err != nil {
+ return fmt.Errorf("无法解析为数字: %s", str)
+ }
+
+ fn.Value = val
+ return nil
+}
+
+// UpdateSalesRequest 修改销量请求结构体
+type UpdateSalesRequest struct {
+ ISBN string `json:"isbn" binding:"required"`
+ DaySale7 FlexNumber `json:"daySale7"`
+ DaySale15 FlexNumber `json:"daySale15"`
+ DaySale30 FlexNumber `json:"daySale30"`
+ DaySale60 FlexNumber `json:"daySale60"`
+ DaySale90 FlexNumber `json:"daySale90"`
+ DaySale180 FlexNumber `json:"daySale180"`
+ DaySale365 FlexNumber `json:"daySale365"`
+}
+
+// SetBookBaseInfoToIll 批量设置违规信息(mysql+Redis)
+func (bc *BookCenterController) SetBookBaseInfoToIll(c *gin.Context) {
+ startTime := time.Now()
+
+ // 1. 解析请求参数
+ var request SetBookBaseInfoToIllRequest
+ if err := c.ShouldBindJSON(&request); err != nil {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
+ return
+ }
+
+ // 2. 处理IDs参数
+ ids, err := bc.parseIDs(request.IDs)
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3001, "ID参数错误", err.Error())
+ return
+ }
+
+ // 3. 准备更新数据
+ updateData := bc.prepareUpdateData(request)
+
+ if len(updateData) == 0 {
+ bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
+ return
+ }
+
+ // 4. 开启事务
+ tx, err := bc.db.Begin()
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5001, "系统错误", "开启事务失败")
+ return
+ }
+
+ // 5. 执行MySQL更新
+ log.Printf("SetBookBaseInfoToIll: 开始执行MySQL更新,IDs: %v, updateData: %+v", ids, updateData)
+ rowsAffected, err := bc.updateMySQL(tx, ids, updateData)
+ if err != nil {
+ tx.Rollback()
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5002, "系统错误", "数据库更新失败")
+ return
+ }
+
+ if rowsAffected == 0 {
+ tx.Rollback()
+ bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
+ return
+ }
+
+ // 6. 获取这些ID对应的ISBN列表
+ isbns, err := bc.getISBNsByIDs(tx, ids)
+ if err != nil {
+ tx.Rollback()
+ log.Printf("获取ISBN列表失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5003, "系统错误", "获取ISBN列表失败")
+ return
+ }
+
+ // 7. 更新Redis
+ if err := bc.updateBookInfoInRedis(isbns, updateData); err != nil {
+ tx.Rollback()
+ log.Printf("Redis更新失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5004, "系统错误", "Redis更新失败")
+ return
+ }
+
+ // 8. 提交事务
+ if err := tx.Commit(); err != nil {
+ log.Printf("提交事务失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5005, "系统错误", "提交事务失败")
+ return
+ }
+
+ // 9. 清除相关的查询缓存
+ if err := bc.clearQueryCache(); err != nil {
+ log.Printf("清除查询缓存失败: %v", err)
+ // 不影响主流程,只记录日志
+ }
+
+ // 10. 记录处理耗时
+ elapsed := time.Since(startTime)
+ log.Printf("批量更新完成,影响行数: %d, 处理耗时: %v", rowsAffected, elapsed)
+
+ // 11. 返回成功响应
+ bc.sendSuccessResponse(c, http.StatusOK, "修改成功", gin.H{
+ "rows_affected": rowsAffected,
+ "books_updated": len(isbns),
+ "time_cost": elapsed.String(),
+ })
+}
+
+// SetBookBaseInfoToIllByISBN 根据ISBN批量设置违规信息(mysql+Redis)
+func (bc *BookCenterController) SetBookBaseInfoToIllByISBN(c *gin.Context) {
+ startTime := time.Now()
+
+ // 1. 解析请求参数
+ var request SetBookBaseInfoToIllByISBNRequest
+ if err := c.ShouldBindJSON(&request); err != nil {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
+ return
+ }
+
+ // 2. 处理ISBNs参数
+ isbns, err := bc.parseISBNs(request.ISBNs)
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3001, "ISBN参数错误", err.Error())
+ return
+ }
+
+ // 3. 准备更新数据
+ updateData := bc.prepareUpdateDataByISBN(request)
+
+ if len(updateData) == 0 {
+ bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
+ return
+ }
+
+ // 4. 开启事务
+ tx, err := bc.db.Begin()
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5001, "系统错误", "开启事务失败")
+ return
+ }
+
+ // 5. 执行MySQL更新(根据ISBN)
+ log.Printf("SetBookBaseInfoToIllByISBN: 开始执行MySQL更新,ISBNs: %v, updateData: %+v", isbns, updateData)
+ rowsAffected, err := bc.updateMySQLByISBN(tx, isbns, updateData)
+ if err != nil {
+ tx.Rollback()
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5002, "系统错误", "数据库更新失败")
+ return
+ }
+
+ if rowsAffected == 0 {
+ tx.Rollback()
+ bc.sendSuccessResponse(c, http.StatusOK, "暂未修改", nil)
+ return
+ }
+
+ // 6. 更新Redis
+ if err := bc.updateBookInfoInRedis(isbns, updateData); err != nil {
+ tx.Rollback()
+ log.Printf("Redis更新失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5004, "系统错误", "Redis更新失败")
+ return
+ }
+
+ // 7. 提交事务
+ if err := tx.Commit(); err != nil {
+ log.Printf("提交事务失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5005, "系统错误", "提交事务失败")
+ return
+ }
+
+ // 8. 清除相关的查询缓存
+ if err := bc.clearQueryCache(); err != nil {
+ log.Printf("清除查询缓存失败: %v", err)
+ // 不影响主流程,只记录日志
+ }
+
+ // 9. 记录处理耗时
+ elapsed := time.Since(startTime)
+ log.Printf("根据ISBN批量更新完成,影响行数: %d, 处理耗时: %v", rowsAffected, elapsed)
+
+ // 10. 返回成功响应
+ bc.sendSuccessResponse(c, http.StatusOK, "修改成功", gin.H{
+ "rows_affected": rowsAffected,
+ "books_updated": len(isbns),
+ "time_cost": elapsed.String(),
+ })
+}
+
+// clearQueryCache 清除查询缓存
+func (bc *BookCenterController) clearQueryCache() error {
+ // 获取所有以 "book_query:" 开头的缓存键
+ items := bc.cache.Items()
+ var keysToDelete []string
+
+ for key := range items {
+ if strings.HasPrefix(key, "book_query:") {
+ keysToDelete = append(keysToDelete, key)
+ }
+ }
+
+ // 删除所有查询缓存
+ for _, key := range keysToDelete {
+ bc.cache.Delete(key)
+ }
+
+ log.Printf("清除了 %d 个查询缓存", len(keysToDelete))
+ return nil
+}
+
+// parseIDs 解析IDs参数
+func (bc *BookCenterController) parseIDs(ids interface{}) ([]int, error) {
+ var result []int
+
+ switch v := ids.(type) {
+ case string:
+ if v == "" {
+ return nil, fmt.Errorf("必备参数为空ids")
+ }
+ strIDs := strings.Split(v, ",")
+ for _, strID := range strIDs {
+ id, err := strconv.Atoi(strID)
+ if err != nil {
+ return nil, fmt.Errorf("ID格式错误: %s", strID)
+ }
+ result = append(result, id)
+ }
+ case []interface{}:
+ if len(v) == 0 {
+ return nil, fmt.Errorf("必备参数为空ids")
+ }
+ for _, id := range v {
+ switch idVal := id.(type) {
+ case float64:
+ result = append(result, int(idVal))
+ case string:
+ idInt, err := strconv.Atoi(idVal)
+ if err != nil {
+ return nil, fmt.Errorf("ID格式错误: %s", idVal)
+ }
+ result = append(result, idInt)
+ default:
+ return nil, fmt.Errorf("ID类型错误,必须是字符串或数字")
+ }
+ }
+ default:
+ return nil, fmt.Errorf("ID类型错误,必须是字符串或数组")
+ }
+
+ if len(result) == 0 {
+ return nil, fmt.Errorf("未提供有效的ID")
+ }
+
+ return result, nil
+}
+
+// prepareUpdateData 准备要更新的数据
+func (bc *BookCenterController) prepareUpdateData(request SetBookBaseInfoToIllRequest) map[string]interface{} {
+ updateData := make(map[string]interface{})
+
+ if request.VioBook != nil {
+ updateData["vio_book"] = *request.VioBook
+ }
+ if request.BookSet != nil {
+ updateData["book_set"] = *request.BookSet
+ }
+ if request.OnenumMbooks != nil {
+ updateData["onenum_mbooks"] = *request.OnenumMbooks
+ }
+ if request.IllPublisher != nil {
+ updateData["ill_publisher"] = *request.IllPublisher
+ }
+ if request.IllAuthor != nil {
+ updateData["ill_author"] = *request.IllAuthor
+ }
+
+ return updateData
+}
+
+// updateMySQL 更新MySQL数据库
+func (bc *BookCenterController) updateMySQL(tx *sql.Tx, ids []int, updateData map[string]interface{}) (int64, error) {
+ log.Printf("updateMySQL: 开始构建SQL查询,IDs: %v, updateData: %+v", ids, updateData)
+
+ // 构建SQL查询
+ query := "UPDATE book_center SET "
+ var setParts []string
+ var args []interface{}
+
+ for field, value := range updateData {
+ setParts = append(setParts, field+" = ?")
+ args = append(args, value)
+ log.Printf("updateMySQL: 添加更新字段: %s = %v", field, value)
+ }
+ query += strings.Join(setParts, ", ") + " WHERE id IN ("
+
+ // 添加ID占位符
+ placeholders := make([]string, len(ids))
+ for i := range ids {
+ placeholders[i] = "?"
+ args = append(args, ids[i])
+ }
+ query += strings.Join(placeholders, ",") + ")"
+
+ log.Printf("updateMySQL: 构建的SQL查询: %s", query)
+ log.Printf("updateMySQL: SQL参数: %v", args)
+
+ // 执行MySQL更新
+ log.Printf("updateMySQL: 开始执行SQL更新")
+ result, err := tx.Exec(query, args...)
+ if err != nil {
+ log.Printf("updateMySQL: ❌ SQL执行失败: %v", err)
+ return 0, fmt.Errorf("数据库更新错误: %v", err)
+ }
+ log.Printf("updateMySQL: ✅ SQL执行成功")
+
+ // 检查影响的行数
+ rowsAffected, err := result.RowsAffected()
+ if err != nil {
+ log.Printf("updateMySQL: ❌ 获取影响行数失败: %v", err)
+ return 0, fmt.Errorf("获取影响行数错误: %v", err)
+ }
+
+ log.Printf("updateMySQL: ✅ 更新完成,影响行数: %d", rowsAffected)
+ return rowsAffected, nil
+}
+
+// getISBNsByIDs 根据ID列表获取对应的ISBN列表
+func (bc *BookCenterController) getISBNsByIDs(tx *sql.Tx, ids []int) ([]string, error) {
+ query := "SELECT isbn FROM book_center WHERE id IN ("
+ placeholders := make([]string, len(ids))
+ args := make([]interface{}, len(ids))
+ for i, id := range ids {
+ placeholders[i] = "?"
+ args[i] = id
+ }
+ query += strings.Join(placeholders, ",") + ")"
+
+ rows, err := tx.Query(query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("查询ISBN失败: %v", err)
+ }
+ defer rows.Close()
+
+ var isbns []string
+ for rows.Next() {
+ var isbn string
+ if err := rows.Scan(&isbn); err != nil {
+ return nil, fmt.Errorf("扫描ISBN失败: %v", err)
+ }
+ isbns = append(isbns, isbn)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, fmt.Errorf("行遍历错误: %v", err)
+ }
+
+ return isbns, nil
+}
+
+// updateBookInfoInRedis 更新Redis中的书籍信息
+func (bc *BookCenterController) updateBookInfoInRedis(isbns []string, updateData map[string]interface{}) error {
+ log.Printf("updateBookInfoInRedis: 开始更新Redis,ISBNs: %v, updateData: %+v", isbns, updateData)
+
+ ctx := context.Background()
+ pipe := bc.redis.Pipeline()
+
+ // 定义允许更新的字段列表
+ allowedFields := map[string]bool{
+ "vio_book": true,
+ "book_set": true,
+ "onenum_mbooks": true,
+ "ill_publisher": true,
+ "ill_author": true,
+ "book_name": true,
+ "book_pic": 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,
+ "update_time": true,
+ }
+
+ // 准备要更新的字段
+ fieldsToUpdate := make(map[string]interface{})
+ for field, value := range updateData {
+ if allowedFields[field] {
+ // 处理int和string两种类型
+ switch v := value.(type) {
+ case int:
+ fieldsToUpdate[field] = v
+ case string:
+ fieldsToUpdate[field] = v
+ default:
+ log.Printf("警告: 字段 %s 的值类型不支持: %T", field, value)
+ continue
+ }
+ }
+ }
+
+ if len(fieldsToUpdate) == 0 {
+ return fmt.Errorf("没有有效的字段需要更新")
+ }
+
+ // 批量更新Redis
+ for _, isbn := range isbns {
+ key := fmt.Sprintf(isbn)
+ log.Printf("updateBookInfoInRedis: 处理ISBN: %s", isbn)
+
+ // 先获取现有数据
+ existingData, err := bc.redis.Get(ctx, key).Result()
+ if err != nil && err != redis.Nil {
+ log.Printf("updateBookInfoInRedis: 获取Redis数据失败(key: %s): %v", key, err)
+ return fmt.Errorf("获取Redis数据失败(key: %s): %v", key, err)
+ }
+
+ var bookInfo BookInfo
+ if existingData != "" {
+ log.Printf("updateBookInfoInRedis: 找到现有数据,长度: %d", len(existingData))
+ if err := json.Unmarshal([]byte(existingData), &bookInfo); err != nil {
+ log.Printf("updateBookInfoInRedis: 解析Redis数据失败(key: %s): %v, 原始数据: %s", key, err, existingData)
+ return fmt.Errorf("解析Redis数据失败(key: %s): %v", key, err)
+ }
+ log.Printf("updateBookInfoInRedis: 成功解析现有数据,ID: %v", bookInfo.ID)
+ } else {
+ log.Printf("updateBookInfoInRedis: 未找到现有数据,将创建新记录")
+ }
+
+ // 更新字段
+ if val, ok := fieldsToUpdate["vio_book"]; ok {
+ bookInfo.VioBook = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["book_set"]; ok {
+ bookInfo.BookSet = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["onenum_mbooks"]; ok {
+ bookInfo.OnenumMbooks = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["ill_publisher"]; ok {
+ bookInfo.IllPublisher = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["ill_author"]; ok {
+ bookInfo.IllAuthor = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["book_name"]; ok {
+ bookInfo.BookName = val.(string)
+ }
+ if val, ok := fieldsToUpdate["book_pic"]; ok {
+ bookInfo.BookPic = val.(string)
+ }
+ // 销量字段更新
+ if val, ok := fieldsToUpdate["day_sale_7"]; ok {
+ bookInfo.DaySale7 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["day_sale_15"]; ok {
+ bookInfo.DaySale15 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["day_sale_30"]; ok {
+ bookInfo.DaySale30 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["day_sale_60"]; ok {
+ bookInfo.DaySale60 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["day_sale_90"]; ok {
+ bookInfo.DaySale90 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["day_sale_180"]; ok {
+ bookInfo.DaySale180 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["day_sale_365"]; ok {
+ bookInfo.DaySale365 = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["this_year_sale"]; ok {
+ bookInfo.ThisYearSale = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["last_year_sale"]; ok {
+ bookInfo.LastYearSale = FlexInt(val.(int))
+ }
+ if val, ok := fieldsToUpdate["total_sale"]; ok {
+ bookInfo.TotalSale = FlexInt(val.(int))
+ }
+
+ // 序列化并保存回Redis
+ updatedData, err := json.Marshal(bookInfo)
+ if err != nil {
+ return fmt.Errorf("序列化书籍数据失败: %v", err)
+ }
+
+ pipe.Set(ctx, key, updatedData, 0)
+ }
+
+ // 执行pipeline
+ if _, err := pipe.Exec(ctx); err != nil {
+ return fmt.Errorf("执行Redis pipeline失败: %v", err)
+ }
+
+ log.Printf("成功更新 %d 个Redis键", len(isbns))
+ return nil
+}
+
+// sendErrorResponse 发送错误响应
+func (bc *BookCenterController) sendErrorResponse(c *gin.Context, statusCode, code int, msg, subMsg string) {
+ c.JSON(statusCode, gin.H{
+ "errorResponse": map[string]interface{}{
+ "code": code,
+ "msg": msg,
+ "subCode": code,
+ "subMsg": subMsg,
+ },
+ })
+}
+
+// sendSuccessResponse 发送成功响应
+func (bc *BookCenterController) sendSuccessResponse(c *gin.Context, statusCode int, msg string, data interface{}) {
+ response := gin.H{
+ "code": 200,
+ "msg": msg,
+ }
+ if data != nil {
+ response["data"] = data
+ }
+ c.JSON(statusCode, response)
+}
+
+// SetBookBaseInfoBookName 更新图书名称(MySQL+Redis双写)
+func (bc *BookCenterController) SetBookBaseInfoBookName(c *gin.Context) {
+ // 解析请求参数
+ var request struct {
+ ID string `json:"id"`
+ BookName string `json:"book_name"`
+ }
+
+ if err := c.ShouldBindJSON(&request); err != nil {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
+ return
+ }
+
+ // 检查必备参数
+ if request.ID == "" || request.BookName == "" {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "必备参数为空", "id和book_name不能为空")
+ return
+ }
+
+ // 开始数据库事务
+ tx, err := bc.db.Begin()
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "数据库事务启动失败", err.Error())
+ return
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // 1. 执行MySQL更新
+ query := "UPDATE book_center SET book_name = ? WHERE id = ?"
+ result, err := tx.Exec(query, request.BookName, request.ID)
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "数据库更新失败", err.Error())
+ return
+ }
+
+ // 检查影响的行数
+ rowsAffected, err := result.RowsAffected()
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "无法确定更新结果", err.Error())
+ return
+ }
+
+ if rowsAffected == 0 {
+ tx.Rollback()
+ bc.sendSuccessResponse(c, http.StatusOK, "暂未修改(可能ID不存在)", nil)
+ return
+ }
+
+ // 2. 获取对应的ISBN
+ idInt, err := strconv.Atoi(request.ID)
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "ID格式不正确", "ID必须为数字")
+ return
+ }
+
+ isbns, err := bc.getISBNsByIDs(tx, []int{idInt})
+ if err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "获取ISBN失败", err.Error())
+ return
+ }
+
+ if len(isbns) == 0 {
+ tx.Rollback()
+ bc.sendErrorResponse(c, http.StatusNotFound, 404, "未找到对应的ISBN", "")
+ return
+ }
+
+ // 3. 更新Redis缓存
+ updateData := map[string]interface{}{
+ "book_name": request.BookName,
+ }
+ err = bc.updateBookInfoInRedis(isbns, updateData)
+ if err != nil {
+ tx.Rollback()
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "Redis更新失败", err.Error())
+ return
+ }
+
+ // 提交事务
+ if err = tx.Commit(); err != nil {
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "事务提交失败", err.Error())
+ return
+ }
+
+ bc.sendSuccessResponse(c, http.StatusOK, "修改成功", nil)
+}
+
+// UpdateSales 修改销量数据(MySQL+Redis双写)
+func (bc *BookCenterController) UpdateSales(c *gin.Context) {
+ startTime := time.Now()
+
+ log.Printf("=== UpdateSales 接口开始执行 ===")
+ log.Printf("UpdateSales: 请求开始时间: %s", startTime.Format("2006-01-02 15:04:05.000"))
+
+ // 1. 解析请求参数
+ var request UpdateSalesRequest
+ if err := c.ShouldBindJSON(&request); err != nil {
+ log.Printf("UpdateSales: 参数解析失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3000, "参数解析错误", err.Error())
+ return
+ }
+
+ log.Printf("UpdateSales: 成功解析请求参数")
+ log.Printf("UpdateSales: 请求参数详情 - ISBN: %s", request.ISBN)
+ log.Printf("UpdateSales: 销量数据 - 7天: %d, 15天: %d, 30天: %d, 60天: %d, 90天: %d, 180天: %d, 365天: %d",
+ request.DaySale7.Value, request.DaySale15.Value, request.DaySale30.Value,
+ request.DaySale60.Value, request.DaySale90.Value, request.DaySale180.Value, request.DaySale365.Value)
+
+ // 2. 验证ISBN格式
+ if !isValidISBN(request.ISBN) {
+ log.Printf("UpdateSales: ISBN格式验证失败: %s", request.ISBN)
+ bc.sendErrorResponse(c, http.StatusBadRequest, 3001, "ISBN格式不正确", "")
+ return
+ }
+
+ log.Printf("UpdateSales: ISBN格式验证通过: %s", request.ISBN)
+ log.Printf("UpdateSales: 开始检查数据库中是否存在该ISBN")
+
+ // 3. 检查数据库中是否存在该ISBN
+ var bookID int
+ checkQuery := "SELECT id FROM book_center WHERE isbn = ? LIMIT 1"
+ log.Printf("UpdateSales: 执行数据库查询: %s, 参数: %s", checkQuery, request.ISBN)
+
+ err := bc.db.QueryRow(checkQuery, request.ISBN).Scan(&bookID)
+
+ if err == sql.ErrNoRows {
+ // 4. 数据库中不存在,调用外部API获取图书信息并插入
+ log.Printf("UpdateSales: ❌ ISBN %s 不存在于数据库,开始从外部API获取数据", request.ISBN)
+ log.Printf("UpdateSales: 准备调用fetchBookDataFromAPI方法")
+
+ bookData, err := bc.fetchBookDataFromAPI(request.ISBN)
+ if err != nil {
+ log.Printf("UpdateSales: ❌ 从外部API获取数据失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5001, "获取图书信息失败", err.Error())
+ return
+ }
+
+ log.Printf("UpdateSales: ✅ 成功从外部API获取图书数据")
+ log.Printf("UpdateSales: 图书基本信息 - 书名: %v, 作者: %v, 出版社: %v",
+ bookData["book_name"], bookData["author"], bookData["publisher"])
+
+ // 5. 将销量数据添加到bookData中
+ log.Printf("UpdateSales: 开始合并销量数据到图书信息中")
+ bookData["day_sale_7"] = request.DaySale7.Value
+ bookData["day_sale_15"] = request.DaySale15.Value
+ bookData["day_sale_30"] = request.DaySale30.Value
+ bookData["day_sale_60"] = request.DaySale60.Value
+ bookData["day_sale_90"] = request.DaySale90.Value
+ bookData["day_sale_180"] = request.DaySale180.Value
+ bookData["day_sale_365"] = request.DaySale365.Value
+
+ log.Printf("UpdateSales: ✅ 销量数据合并完成")
+
+ // 6. 插入到数据库
+ log.Printf("UpdateSales: 开始调用saveToBookCenter插入数据到MySQL")
+ if err := bc.saveToBookCenter(bookData); err != nil {
+ log.Printf("UpdateSales: ❌ 插入图书数据失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5002, "插入图书数据失败", err.Error())
+ return
+ }
+ log.Printf("UpdateSales: ✅ 成功插入数据到MySQL数据库")
+
+ // 7. 存储到Redis
+ log.Printf("UpdateSales: 开始存储数据到Redis缓存")
+ jsonData, err := json.Marshal(bookData)
+ if err != nil {
+ log.Printf("UpdateSales: ❌ 序列化数据失败: %v", err)
+ } else {
+ if err := bc.redis.Set(context.Background(), request.ISBN, jsonData, 0).Err(); err != nil {
+ log.Printf("UpdateSales: ❌ 存储到Redis失败: %v", err)
+ } else {
+ log.Printf("UpdateSales: ✅ 成功存储到Redis, ISBN: %s", request.ISBN)
+ }
+ }
+
+ elapsed := time.Since(startTime)
+ log.Printf("UpdateSales: 新增图书并设置销量完成,ISBN: %s, 处理耗时: %v", request.ISBN, elapsed)
+
+ // 8. 返回成功响应
+ bc.sendSuccessResponse(c, http.StatusOK, "图书新增并设置销量成功", gin.H{
+ "isbn": request.ISBN,
+ "action": "created_and_updated",
+ "time_cost": elapsed.String(),
+ "updated_fields": gin.H{
+ "day_sale_7": request.DaySale7.Value,
+ "day_sale_15": request.DaySale15.Value,
+ "day_sale_30": request.DaySale30.Value,
+ "day_sale_60": request.DaySale60.Value,
+ "day_sale_90": request.DaySale90.Value,
+ "day_sale_180": request.DaySale180.Value,
+ "day_sale_365": request.DaySale365.Value,
+ },
+ })
+ return
+
+ } else if err != nil {
+ // 数据库查询出错
+ log.Printf("UpdateSales: ❌ 检查ISBN存在性失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5003, "系统错误", "检查图书存在性失败")
+ return
+ }
+
+ // 9. 数据库中存在该ISBN,直接更新销量
+ log.Printf("UpdateSales: ✅ ISBN %s 存在于数据库 (ID: %d),开始更新销量", request.ISBN, bookID)
+ log.Printf("UpdateSales: 准备开启数据库事务进行更新操作")
+
+ // 开启事务
+ tx, err := bc.db.Begin()
+ if err != nil {
+ log.Printf("开启事务失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5004, "系统错误", "开启事务失败")
+ return
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback()
+ }
+ }()
+
+ // 10. 构建更新SQL
+ updateQuery := `UPDATE book_center SET
+ day_sale_7 = ?, day_sale_15 = ?, day_sale_30 = ?, day_sale_60 = ?,
+ day_sale_90 = ?, day_sale_180 = ?, day_sale_365 = ?,
+ update_time = ?
+ WHERE isbn = ?`
+
+ // 11. 执行MySQL更新
+ result, err := tx.Exec(updateQuery,
+ request.DaySale7.Value,
+ request.DaySale15.Value,
+ request.DaySale30.Value,
+ request.DaySale60.Value,
+ request.DaySale90.Value,
+ request.DaySale180.Value,
+ request.DaySale365.Value,
+ time.Now().Unix(),
+ request.ISBN,
+ )
+ if err != nil {
+ log.Printf("MySQL更新失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5005, "系统错误", "数据库更新失败")
+ return
+ }
+
+ // 12. 检查影响的行数
+ rowsAffected, err := result.RowsAffected()
+ if err != nil {
+ log.Printf("获取影响行数失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5006, "系统错误", "获取影响行数失败")
+ return
+ }
+
+ if rowsAffected == 0 {
+ bc.sendErrorResponse(c, http.StatusNotFound, 4004, "更新失败", "未找到对应的图书记录")
+ return
+ }
+
+ // 13. 更新Redis缓存
+ updateData := map[string]interface{}{
+ "day_sale_7": request.DaySale7.Value,
+ "day_sale_15": request.DaySale15.Value,
+ "day_sale_30": request.DaySale30.Value,
+ "day_sale_60": request.DaySale60.Value,
+ "day_sale_90": request.DaySale90.Value,
+ "day_sale_180": request.DaySale180.Value,
+ "day_sale_365": request.DaySale365.Value,
+ "update_time": time.Now().Unix(),
+ }
+
+ err = bc.updateBookInfoInRedis([]string{request.ISBN}, updateData)
+ if err != nil {
+ log.Printf("Redis更新失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5007, "系统错误", "Redis更新失败")
+ return
+ }
+
+ // 14. 提交事务
+ if err = tx.Commit(); err != nil {
+ log.Printf("事务提交失败: %v", err)
+ bc.sendErrorResponse(c, http.StatusInternalServerError, 5008, "系统错误", "事务提交失败")
+ return
+ }
+
+ // 15. 清除相关缓存
+ bc.cache.Flush()
+
+ elapsed := time.Since(startTime)
+ log.Printf("UpdateSales: 销量修改完成,ISBN: %s, 影响行数: %d, 处理耗时: %v",
+ request.ISBN, rowsAffected, elapsed)
+
+ // 16. 返回成功响应
+ bc.sendSuccessResponse(c, http.StatusOK, "销量修改成功", gin.H{
+ "isbn": request.ISBN,
+ "action": "updated",
+ "rows_affected": rowsAffected,
+ "time_cost": elapsed.String(),
+ "updated_fields": gin.H{
+ "day_sale_7": request.DaySale7.Value,
+ "day_sale_15": request.DaySale15.Value,
+ "day_sale_30": request.DaySale30.Value,
+ "day_sale_60": request.DaySale60.Value,
+ "day_sale_90": request.DaySale90.Value,
+ "day_sale_180": request.DaySale180.Value,
+ "day_sale_365": request.DaySale365.Value,
+ },
+ })
+}
+
+// GetBookByID 根据ID查询图书信息
+func (bc *BookCenterController) GetBookByID(c *gin.Context) {
+ // 获取ID参数
+ id := c.Param("id")
+ if id == "" {
+ id = c.Query("id") // 兼容查询参数
+ if id == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ID参数不能为空"})
+ return
+ }
+ }
+
+ // 构建查询语句
+ query := `
+ SELECT
+ id, book_name, book_pic, isbn, author, publisher,
+ publication_time, binding_layout, fix_price,
+ buy_count, sell_count, vio_book, book_set,
+ onenum_mbooks, ill_publisher, ill_author,
+ day_sale_7, day_sale_15, day_sale_30, day_sale_60,
+ day_sale_90, day_sale_180, day_sale_365,
+ this_year_sale, last_year_sale, total_sale
+ FROM book_center
+ WHERE id = ?`
+
+ // 定义接收变量
+ var (
+ idVal, isbn, bookName, bookPic, author, publisher, bindingLayout string
+ publicationTime interface{}
+ fixPrice float64
+ buyCount, sellCount string
+ vioBook, bookSet, onenumMbooks, illPublisher, illAuthor int
+ daySale7, daySale15, daySale30, daySale60, daySale90 int
+ daySale180, daySale365, thisYearSale, lastYearSale, totalSale int
+ )
+
+ // 执行查询
+ err := bc.db.QueryRow(query, id).Scan(
+ &idVal, &bookName, &bookPic, &isbn, &author, &publisher,
+ &publicationTime, &bindingLayout, &fixPrice,
+ &buyCount, &sellCount, &vioBook, &bookSet, &onenumMbooks,
+ &illPublisher, &illAuthor,
+ &daySale7, &daySale15, &daySale30, &daySale60, &daySale90,
+ &daySale180, &daySale365, &thisYearSale, &lastYearSale, &totalSale,
+ )
+
+ if err != nil {
+ if err == sql.ErrNoRows {
+ c.JSON(http.StatusNotFound, gin.H{"error": "未找到对应的图书信息"})
+ } else {
+ log.Printf("数据库查询错误: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": "读取数据失败",
+ "details": err.Error(),
+ })
+ }
+ return
+ }
+ // 构建返回结果(数组格式)
+ bookData := gin.H{
+ "id": idVal,
+ "book_name": bookName,
+ "book_pic": bookPic,
+ "isbn": isbn,
+ "author": author,
+ "publisher": publisher,
+ "publication_time": publicationTime,
+ "binding_layout": bindingLayout,
+ "fix_price": fixPrice,
+ "buy_count": buyCount,
+ "sell_count": sellCount,
+ "vio_book": vioBook,
+ "book_set": bookSet,
+ "onenum_mbooks": onenumMbooks,
+ "ill_publisher": illPublisher,
+ "ill_author": illAuthor,
+ "day_sale_7": daySale7,
+ "day_sale_15": daySale15,
+ "day_sale_30": daySale30,
+ "day_sale_60": daySale60,
+ "day_sale_90": daySale90,
+ "day_sale_180": daySale180,
+ "day_sale_365": daySale365,
+ "this_year_sale": thisYearSale,
+ "last_year_sale": lastYearSale,
+ "total_sale": totalSale,
+ }
+ // 将单个对象包装成数组
+ c.JSON(http.StatusOK, gin.H{
+ "data": []gin.H{bookData}, // 注意这里用[]gin.H将对象转为数组
+ })
+ //// 构建返回结果
+ //c.JSON(http.StatusOK, gin.H{
+ // "data": gin.H{
+ // "id": idVal,
+ // "book_name": bookName,
+ // "book_pic": bookPic,
+ // "isbn": isbn,
+ // "author": author,
+ // "publisher": publisher,
+ // "publication_time": publicationTime, // 直接返回原始值
+ // "binding_layout": bindingLayout,
+ // "fix_price": fixPrice,
+ // "buy_count": buyCount,
+ // "sell_count": sellCount,
+ // "vio_book": vioBook,
+ // "book_set": bookSet,
+ // "onenum_mbooks": onenumMbooks,
+ // "ill_publisher": illPublisher,
+ // "ill_author": illAuthor,
+ // "day_sale_7": daySale7,
+ // "day_sale_15": daySale15,
+ // "day_sale_30": daySale30,
+ // "day_sale_60": daySale60,
+ // "day_sale_90": daySale90,
+ // "day_sale_180": daySale180,
+ // "day_sale_365": daySale365,
+ // "this_year_sale": thisYearSale,
+ // "last_year_sale": lastYearSale,
+ // "total_sale": totalSale,
+ // },
+ //})
+}
+
+// InsertBaseInfo 插入图书基本信息
+type BookBaseInfo struct {
+ Id int `form:"id"`
+ Category string `form:"category"`
+ BookName string `form:"book_name"`
+ BookPic string `form:"book_pic"`
+ BookPicS string `form:"book_pic_s"` // 改为 string 类型
+ Isbn string `form:"isbn"`
+ Author string `form:"author"`
+ Editor string `form:"editor"`
+ BindingLayout string `form:"binding_layout"`
+ Publisher string `form:"publisher"`
+ Edition string `form:"edition"`
+ Format string `form:"format"`
+ Languages string `form:"languages"`
+ PublicationTime FlexInt64 `form:"publication_time"`
+ PrintTime int64 `form:"print_time"`
+ Paper string `form:"paper"`
+ Pages int `form:"pages"`
+ Wordage string `form:"wordage"`
+ FixPrice int64 `form:"fix_price"`
+ Content string `form:"content"`
+ Remark string `form:"remark"`
+ VioBook int `form:"vio_book"`
+ BookSet int `form:"book_set"`
+ OnenumMbooks int `form:"onenum_mbooks"`
+ IllPublisher int `form:"ill_publisher"`
+ IllAuthor int `form:"ill_author"`
+ DaySale7 int `form:"day_sale_7"`
+ DaySale15 int `form:"day_sale_15"`
+ DaySale30 int `form:"day_sale_30"`
+ DaySale60 int `form:"day_sale_60"`
+ DaySale90 int `form:"day_sale_90"`
+ DaySale180 int `form:"day_sale_180"`
+ DaySale365 int `form:"day_sale_365"`
+ ThisYearSale int `form:"this_year_sale"`
+ LastYearSale int `form:"last_year_sale"`
+ TotalSale int `form:"total_sale"`
+ SoldOutTimes string `form:"sold_out_times"`
+ ShipmentCycle int `form:"shipment_cycle"`
+ BuyCount string `form:"buy_count"`
+ SellCount string `form:"sell_count"`
+ PublictionTimes interface{} `form:"publiction_times"`
+ CatId int `form:"cat_id"`
+ BuyCounts int64 `form:"buy_counts"`
+ SellCounts int64 `form:"sell_counts"`
+ CreateBy int64 `form:"create_by"`
+ CreateTime int64 `form:"create_time"`
+ UpdateBy int64 `form:"update_by"`
+ UpdateTime int64 `form:"update_time"`
+}
+
+func getISBNValue(apiISBN, fallbackISBN string) string {
+ if apiISBN == "" {
+ return fallbackISBN
+ }
+ return apiISBN
+}
+
+// generateISBNHash 生成ISBN的数字哈希值(用于分区)
+func generateISBNHash(isbn string) uint64 {
+ hash := md5.Sum([]byte(isbn))
+ // 取MD5哈希的前8个字节转换为uint64
+ var result uint64
+ for i := 0; i < 8; i++ {
+ result = (result << 8) | uint64(hash[i])
+ }
+ return result
+}
+
+// fetchBookDataFromAPI 从外部API获取图书数据
+func (bc *BookCenterController) fetchBookDataFromAPI(isbn string) (map[string]interface{}, error) {
+ log.Printf("fetchBookDataFromAPI: 开始从外部API获取图书数据, ISBN: %s", isbn)
+
+ // 调用外部API
+ apiUrl := detailPagesUrl + "/api/image-url?isbn=" + isbn
+ log.Printf("fetchBookDataFromAPI: 调用外部API: %s", apiUrl)
+
+ resp, err := http.Get(apiUrl)
+ if err != nil {
+ return nil, fmt.Errorf("调用外部API失败: %w", err)
+ }
+ defer resp.Body.Close()
+
+ log.Printf("fetchBookDataFromAPI: 外部API响应状态码: %d", resp.StatusCode)
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("外部API返回错误状态码: %d", resp.StatusCode)
+ }
+
+ // 读取响应体
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("读取API响应体失败: %w", err)
+ }
+
+ log.Printf("fetchBookDataFromAPI: 外部API原始响应数据: %s", string(bodyBytes))
+
+ // 解析API响应
+ var apiResponse struct {
+ Success bool `json:"success"`
+ Data struct {
+ Detail struct {
+ Editor string `json:"editor"`
+ Languages string `json:"languages"`
+ PrintTime string `json:"print_time"`
+ ISBN string `json:"isbn"`
+ BookPic string `json:"book_pic"`
+ BookName string `json:"book_name"`
+ Author string `json:"author"`
+ Format string `json:"format"`
+ Publisher string `json:"publisher"`
+ Paper string `json:"paper"`
+ PublicationTime FlexInt64 `json:"publication_time"`
+ Pages string `json:"pages"`
+ Edition string `json:"edition"`
+ Wordage string `json:"wordage"`
+ Category string `json:"category"`
+ FixPrice string `json:"fix_price"`
+ BindingLayout string `json:"binding_layout"`
+ BuyCount string `json:"buyCount"`
+ SellCount string `json:"sellCount"`
+ Content string `json:"content"`
+ } `json:"detail"`
+ ImageURL string `json:"image_url"`
+ } `json:"data"`
+ }
+
+ if err := json.Unmarshal(bodyBytes, &apiResponse); err != nil {
+ return nil, fmt.Errorf("解析API响应失败: %w", err)
+ }
+
+ log.Printf("fetchBookDataFromAPI: 解析后的API响应: %+v", apiResponse)
+
+ // 转换数据格式
+ transformedData := map[string]interface{}{
+ "author": apiResponse.Data.Detail.Author,
+ "binding_layout": apiResponse.Data.Detail.BindingLayout,
+ "book_name": apiResponse.Data.Detail.BookName,
+ "book_pic": apiResponse.Data.ImageURL,
+ "book_pic_s": nil,
+ "book_set": 0,
+ "buy_count": apiResponse.Data.Detail.BuyCount,
+ "buy_counts": parseCount(apiResponse.Data.Detail.BuyCount),
+ "cat_id": 0,
+ "category": apiResponse.Data.Detail.Category,
+ "content": apiResponse.Data.Detail.Content,
+ "create_by": 1,
+ "create_time": time.Now().Unix(),
+ "day_sale_15": 0,
+ "day_sale_180": 0,
+ "day_sale_30": 0,
+ "day_sale_365": 0,
+ "day_sale_60": 0,
+ "day_sale_7": 0,
+ "day_sale_90": 0,
+ "edition": apiResponse.Data.Detail.Edition,
+ "editor": apiResponse.Data.Detail.Editor,
+ "fix_price": parsePrice(apiResponse.Data.Detail.FixPrice),
+ "format": apiResponse.Data.Detail.Format,
+ "id": 0,
+ "ill_author": 0,
+ "ill_publisher": 0,
+ "isbn": getISBNValue(apiResponse.Data.Detail.ISBN, isbn),
+ "languages": apiResponse.Data.Detail.Languages,
+ "last_year_sale": 0,
+ "onenum_mbooks": 0,
+ "pages": parseCount(apiResponse.Data.Detail.Pages),
+ "paper": apiResponse.Data.Detail.Paper,
+ "print_time": parseTime(apiResponse.Data.Detail.PrintTime),
+ "publication_time": apiResponse.Data.Detail.PublicationTime,
+ "publiction_times": apiResponse.Data.Detail.PublicationTime,
+ "publisher": apiResponse.Data.Detail.Publisher,
+ "remark": "",
+ "sell_count": apiResponse.Data.Detail.SellCount,
+ "sell_counts": parseCount(apiResponse.Data.Detail.SellCount),
+ "shipment_cycle": 0,
+ "sold_out_times": "[]",
+ "this_year_sale": 0,
+ "total_sale": 0,
+ "update_by": 0,
+ "update_time": time.Now().Unix(),
+ "vio_book": 0,
+ "wordage": apiResponse.Data.Detail.Wordage,
+ }
+
+ log.Printf("fetchBookDataFromAPI: 数据转换完成, ISBN: %s", isbn)
+ return transformedData, nil
+}
+func (bc *BookCenterController) InsertBaseInfo(c *gin.Context) {
+ // 1. 解析请求数据
+ var book BookBaseInfo
+ //if err := c.ShouldBindJSON(&book); err != nil {
+ // c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数解析失败: " + err.Error()})
+ // return
+ //}
+ if err := c.ShouldBind(&book); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数解析失败: " + err.Error()})
+ return
+ }
+ // 将 book.BookPicS 转换为 sql.NullString
+ bookPicS := sql.NullString{
+ String: book.BookPicS,
+ Valid: book.BookPicS != "", // 如果非空,则 Valid=true
+ }
+
+ // 如果 BookPicS 为空,设置默认值 {}
+ if !bookPicS.Valid {
+ bookPicS = sql.NullString{
+ String: "{}",
+ Valid: true,
+ }
+ }
+ err1 := json.Unmarshal([]byte(book.BookPicS), &bookPicS)
+ if err1 != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数解析失败: " + err1.Error()})
+ return
+ }
+
+ // 调试日志 - 打印接收到的原始数据
+ log.Printf("Received book_pic_s raw value: %v", book.BookPicS)
+ // 2. 检查ISBN是否为空
+ if book.Isbn == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN不能为空"})
+ return
+ }
+
+ // 3. 检查图书是否已存在
+ //var existingID int
+ //err := bc.db.QueryRow("SELECT * FROM book_center WHERE isbn = ?", book.Isbn).Scan(&existingID)
+ //将查询出的结果存储到变量中
+
+ //// 4. 获取分布式锁(防止并发冲突)
+ //lockKey := fmt.Sprintf("lock:%s", book.Isbn)
+ //locked, err2 := bc.redis.SetNX(context.Background(), lockKey, "1", 10*time.Second).Result()
+ //if err2 != nil || !locked {
+ // c.JSON(http.StatusConflict, gin.H{"error": "系统繁忙,请稍后重试"})
+ // return
+ //}
+ //defer bc.redis.Del(context.Background(), lockKey)
+
+ var dbBook BookBaseInfo
+
+ //err := bc.db.QueryRow("SELECT id,category,book_name,book_pic,book_pic_s,isbn,author,editor,binding_layout,publisher,edition,format," +
+ // "languages,publication_time,print_time,paper,pages,wordage,fix_price,content,remark,vio_book,book_set,onenum_mbooks,ill_publisher," +
+ // "ill_author ,day_sale_7,day_sale_15,day_sale_30,day_sale_60,day_sale_90,day_sale_180,day_sale_365,this_year_sale,last_year_sale," +
+ // "total_sale,sold_out_times,shipment_cycle,buy_count,sell_count,publiction_times,cat_id,buy_counts,sell_counts,create_by,create_time," +
+ // "`update_by,update_time FROM book_center WHERE isbn = ?`", book.Isbn).Scan
+ //(
+ //err1 = bc.db.QueryRow("SELECT id,category,book_name,book_pic,COALESCE(book_pic_s, '{}') AS book_pic_s,isbn,author,editor,binding_layout,publisher,edition,format,"+
+ // "languages,publication_time,print_time,paper,pages,wordage,fix_price,content,remark,vio_book,book_set,onenum_mbooks,ill_publisher,"+
+ // "ill_author,day_sale_7,day_sale_15,day_sale_30,day_sale_60,day_sale_90,day_sale_180,day_sale_365,this_year_sale,last_year_sale,"+
+ // "total_sale,sold_out_times,shipment_cycle,buy_count,sell_count,publiction_times,cat_id,buy_counts,sell_counts,create_by,create_time,"+
+ // "update_by,update_time FROM book_center WHERE isbn = ?", book.Isbn).Scan(
+ // &dbBook.Id,
+ // &dbBook.Category,
+ // &dbBook.BookName,
+ // &dbBook.BookPic,
+ // &dbBook.BookPicS,
+ // &dbBook.Isbn,
+ // &dbBook.Author,
+ // &dbBook.Editor,
+ // &dbBook.BindingLayout,
+ // &dbBook.Publisher,
+ // &dbBook.Edition,
+ // &dbBook.Format,
+ // &dbBook.Languages,
+ // &dbBook.PublicationTime,
+ // &dbBook.PrintTime,
+ // &dbBook.Paper,
+ // &dbBook.Pages,
+ // &dbBook.Wordage,
+ // &dbBook.FixPrice,
+ // &dbBook.Content,
+ // &dbBook.Remark,
+ // &dbBook.VioBook,
+ // &dbBook.BookSet,
+ // &dbBook.OnenumMbooks,
+ // &dbBook.IllPublisher,
+ // &dbBook.IllAuthor,
+ // &dbBook.DaySale7,
+ // &dbBook.DaySale15,
+ // &dbBook.DaySale30,
+ // &dbBook.DaySale60,
+ // &dbBook.DaySale90,
+ // &dbBook.DaySale180,
+ // &dbBook.DaySale365,
+ // &dbBook.ThisYearSale,
+ // &dbBook.LastYearSale,
+ // &dbBook.TotalSale,
+ // &dbBook.SoldOutTimes,
+ // &dbBook.ShipmentCycle,
+ // &dbBook.BuyCount,
+ // &dbBook.SellCount,
+ // &dbBook.PublictionTimes,
+ // &dbBook.CatId,
+ // &dbBook.BuyCounts,
+ // &dbBook.SellCounts,
+ // &dbBook.CreateBy,
+ // &dbBook.CreateTime,
+ // &dbBook.UpdateBy,
+ // &dbBook.UpdateTime,
+ //)
+ err1 = bc.db.QueryRow(`
+ SELECT
+ id, category, book_name, book_pic,
+ COALESCE(book_pic_s, '""') AS book_pic_s, -- 如果 NULL,返回 '{}'
+ isbn, author, editor, binding_layout, publisher,
+ edition, format, languages,
+ COALESCE(publication_time, 0) AS publication_time, -- 如果 NULL,返回 0
+ COALESCE(print_time, 0) AS print_time,
+ COALESCE(paper, '') AS paper,
+ COALESCE(pages, 0) AS pages,
+ COALESCE(wordage, '') AS wordage,
+ COALESCE(fix_price, 0) AS fix_price,
+ COALESCE(content, '') AS content,
+ COALESCE(remark, '') AS remark,
+ COALESCE(vio_book, 0) AS vio_book,
+ COALESCE(book_set, 0) AS book_set,
+ COALESCE(onenum_mbooks, 0) AS onenum_mbooks,
+ COALESCE(ill_publisher, 0) AS ill_publisher,
+ COALESCE(ill_author, 0) AS ill_author,
+ COALESCE(day_sale_7, 0) AS day_sale_7,
+ COALESCE(day_sale_15, 0) AS day_sale_15,
+ COALESCE(day_sale_30, 0) AS day_sale_30,
+ COALESCE(day_sale_60, 0) AS day_sale_60,
+ COALESCE(day_sale_90, 0) AS day_sale_90,
+ COALESCE(day_sale_180, 0) AS day_sale_180,
+ COALESCE(day_sale_365, 0) AS day_sale_365,
+ COALESCE(this_year_sale, 0) AS this_year_sale,
+ COALESCE(last_year_sale, 0) AS last_year_sale,
+ COALESCE(total_sale, 0) AS total_sale,
+ COALESCE(sold_out_times, '') AS sold_out_times,
+ COALESCE(shipment_cycle, 0) AS shipment_cycle,
+ COALESCE(buy_counts, '') AS buy_count,
+ COALESCE(sell_counts, '') AS sell_count,
+ COALESCE(publiction_times, 0) AS publiction_times,
+ COALESCE(cat_id, 0) AS cat_id,
+ COALESCE(buy_counts, 0) AS buy_counts,
+ COALESCE(sell_counts, 0) AS sell_counts,
+ COALESCE(create_by, 0) AS create_by,
+ COALESCE(create_time, 0) AS create_time,
+ COALESCE(update_by, 0) AS update_by,
+ COALESCE(update_time, 0) AS update_time
+ FROM book_center
+ WHERE isbn = ?`,
+ book.Isbn,
+ ).Scan(
+ &dbBook.Id,
+ &dbBook.Category,
+ &dbBook.BookName,
+ &dbBook.BookPic,
+ &dbBook.BookPicS,
+ &dbBook.Isbn,
+ &dbBook.Author,
+ &dbBook.Editor,
+ &dbBook.BindingLayout,
+ &dbBook.Publisher,
+ &dbBook.Edition,
+ &dbBook.Format,
+ &dbBook.Languages,
+ &dbBook.PublicationTime,
+ &dbBook.PrintTime,
+ &dbBook.Paper,
+ &dbBook.Pages,
+ &dbBook.Wordage,
+ &dbBook.FixPrice,
+ &dbBook.Content,
+ &dbBook.Remark,
+ &dbBook.VioBook,
+ &dbBook.BookSet,
+ &dbBook.OnenumMbooks,
+ &dbBook.IllPublisher,
+ &dbBook.IllAuthor,
+ &dbBook.DaySale7,
+ &dbBook.DaySale15,
+ &dbBook.DaySale30,
+ &dbBook.DaySale60,
+ &dbBook.DaySale90,
+ &dbBook.DaySale180,
+ &dbBook.DaySale365,
+ &dbBook.ThisYearSale,
+ &dbBook.LastYearSale,
+ &dbBook.TotalSale,
+ &dbBook.SoldOutTimes,
+ &dbBook.ShipmentCycle,
+ &dbBook.BuyCount,
+ &dbBook.SellCount,
+ &dbBook.PublictionTimes,
+ &dbBook.CatId,
+ &dbBook.BuyCounts,
+ &dbBook.SellCounts,
+ &dbBook.CreateBy,
+ &dbBook.CreateTime,
+ &dbBook.UpdateBy,
+ &dbBook.UpdateTime,
+ )
+
+ //if err != nil {
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败: " + err.Error()})
+ // return
+ //}
+
+ // 4. 事务处理MySQL操作
+ tx, err := bc.db.Begin()
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库事务启动失败: " + err.Error()})
+ return
+ }
+ mergeBooks(&book, &dbBook)
+
+ var action string
+ if book.BookName != "" {
+ dbBook.BookName = book.BookName
+ }
+ if book.Author != "" {
+ dbBook.Author = book.Author
+ }
+ if book.Publisher != "" {
+ dbBook.Publisher = book.Publisher
+ }
+ if book.PublicationTime != 0 {
+ dbBook.PublicationTime = book.PublicationTime
+ }
+ if book.BindingLayout != "" {
+ dbBook.BindingLayout = book.BindingLayout
+ }
+ if book.FixPrice != 0 {
+ dbBook.FixPrice = book.FixPrice
+ }
+ if book.BookPic != "" {
+ dbBook.BookPic = book.BookPic
+ }
+ if book.BookPicS != "" {
+ dbBook.BookPicS = book.BookPicS
+ }
+
+ if err1 == nil {
+ // ISBN已存在,执行更新操作
+ _, err = tx.Exec(`
+ UPDATE book_center SET
+ category = ?, book_name = ?, book_pic = ?, book_pic_s = ?,
+ author = ?, editor = ?, binding_layout = ?, publisher = ?,
+ edition = ?, format = ?, languages = ?, publication_time = ?,
+ print_time = ?, paper = ?, pages = ?, wordage = ?, fix_price = ?,
+ content = ?, remark = ?, vio_book = ?, book_set = ?,
+ onenum_mbooks = ?, ill_publisher = ?, ill_author = ?,
+ update_by = ?, update_time = ?
+ WHERE isbn = ?`,
+ dbBook.Category, dbBook.BookName, dbBook.BookPic, book.BookPicS,
+ dbBook.Author, dbBook.Editor, dbBook.BindingLayout, dbBook.Publisher,
+ dbBook.Edition, dbBook.Format, dbBook.Languages, dbBook.PublicationTime,
+ dbBook.PrintTime, dbBook.Paper, dbBook.Pages, dbBook.Wordage, dbBook.FixPrice,
+ dbBook.Content, dbBook.Remark, dbBook.VioBook, dbBook.BookSet,
+ dbBook.OnenumMbooks, dbBook.IllPublisher, dbBook.IllAuthor,
+ dbBook.UpdateBy, time.Now().Unix(),
+ dbBook.Isbn)
+ action = "更新"
+ } else if err1 == sql.ErrNoRows {
+ // ISBN不存在,执行插入操作
+ _, err = tx.Exec(`
+ INSERT INTO book_center (
+ category, book_name, book_pic, book_pic_s, isbn, author, editor,
+ binding_layout, publisher, edition, format, languages, publication_time,
+ print_time, paper, pages, wordage, fix_price, content, remark,
+ vio_book, book_set, onenum_mbooks, ill_publisher, ill_author,
+ create_by, create_time, update_by, update_time
+ )
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ book.Category, book.BookName, book.BookPic, book.BookPicS, book.Isbn,
+ book.Author, book.Editor, book.BindingLayout, book.Publisher, book.Edition,
+ book.Format, book.Languages, book.PublicationTime, book.PrintTime,
+ book.Paper, book.Pages, book.Wordage, book.FixPrice, book.Content, book.Remark,
+ book.VioBook, book.BookSet, book.OnenumMbooks, book.IllPublisher, book.IllAuthor,
+ book.CreateBy, time.Now().Unix(), book.UpdateBy, time.Now().Unix())
+ action = "新增"
+ } else {
+ tx.Rollback()
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败: " + err1.Error()})
+ return
+ }
+
+ if err != nil {
+ tx.Rollback()
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库操作失败: " + err.Error()})
+ return
+ }
+
+ // 5. 更新Redis数据
+ redisKey := fmt.Sprintf(book.Isbn)
+ bookMap := map[string]interface{}{
+ "id": dbBook.Id,
+ "book_name": dbBook.BookName, // 手动改为小写下划线
+ "isbn": dbBook.Isbn,
+ "author": dbBook.Author,
+ "book_pic": dbBook.BookPic,
+ "book_pic_s": dbBook.BookPicS,
+ "editor": dbBook.Editor,
+ "publisher": dbBook.Publisher,
+ "edition": dbBook.Edition,
+ "format": dbBook.Format,
+ "languages": dbBook.Languages,
+ "publication_time": dbBook.PublicationTime,
+ "print_time": dbBook.PrintTime,
+ "paper": dbBook.Paper,
+ "pages": dbBook.Pages,
+ "wordage": dbBook.Wordage,
+ "fix_price": dbBook.FixPrice,
+ "content": dbBook.Content,
+ "remark": dbBook.Remark,
+ "vio_book": dbBook.VioBook,
+ "book_set": dbBook.BookSet,
+ "onenum_mbooks": dbBook.OnenumMbooks,
+ "ill_publisher": dbBook.IllPublisher,
+ "ill_author": dbBook.IllAuthor,
+ "day_sale_7": dbBook.DaySale7,
+ "day_sale_15": dbBook.DaySale15,
+ "day_sale_30": dbBook.DaySale30,
+ "day_sale_60": dbBook.DaySale60,
+ "day_sale_90": dbBook.DaySale90,
+ "day_sale_180": dbBook.DaySale180,
+ "day_sale_365": dbBook.DaySale365,
+ "this_year_sale": dbBook.ThisYearSale,
+ "last_year_sale": dbBook.LastYearSale,
+ "total_sale": dbBook.TotalSale,
+ "sold_out_times": dbBook.SoldOutTimes,
+ "shipment_cycle": dbBook.ShipmentCycle,
+ "buy_count": dbBook.BuyCount,
+ "sell_count": dbBook.SellCount,
+ "publiction_times": dbBook.PublictionTimes,
+ "cat_id": dbBook.CatId,
+ "buy_counts": dbBook.BuyCounts,
+ "sell_counts": dbBook.SellCounts,
+ "create_by": dbBook.CreateBy,
+ "create_time": dbBook.CreateTime,
+ "update_by": dbBook.UpdateBy,
+ "update_time": dbBook.UpdateTime,
+ "category": dbBook.Category,
+ }
+ bookJSON, err := json.Marshal(bookMap)
+ if err != nil {
+ tx.Rollback()
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据序列化失败: " + err.Error()})
+ return
+ }
+
+ // 修改点4:添加Redis操作日志便于调试
+ log.Printf("Updating Redis key: %s with data: %s", redisKey, string(bookJSON))
+
+ // 更新Redis - 现在会在第一次请求时就执行
+ err = bc.redis.Set(context.Background(), redisKey, bookJSON, 0).Err()
+ if err != nil {
+ tx.Rollback()
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Redis操作失败: " + err.Error()})
+ return
+ }
+
+ // 6. 提交事务
+ if err := tx.Commit(); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库事务提交失败: " + err.Error()})
+ return
+ }
+
+ // 7. 返回成功响应
+ c.JSON(http.StatusOK, gin.H{
+ "message": fmt.Sprintf("图书信息%s成功", action),
+ "isbn": dbBook.Isbn,
+ })
+}
+
+func mergeBooks(dest, src *BookBaseInfo) {
+ destVal := reflect.ValueOf(dest).Elem()
+ srcVal := reflect.ValueOf(src).Elem()
+
+ for i := 0; i < destVal.NumField(); i++ {
+ destField := destVal.Field(i)
+ srcField := srcVal.Field(i)
+
+ // 只处理可设置字段且源值不为零值的情况
+ if destField.CanSet() && !reflect.DeepEqual(srcField.Interface(), reflect.Zero(srcField.Type()).Interface()) {
+ // 如果目标值为零值,则用源值替换
+ if reflect.DeepEqual(destField.Interface(), reflect.Zero(destField.Type()).Interface()) {
+ destField.Set(srcField)
+ }
+ }
+ }
+}
+
+// 导出Excel表
+func (bc *BookCenterController) exportISBNs(c *gin.Context) {
+ // 查询所有ISBN记录(不限制数量)
+ query := "SELECT isbn FROM book_center WHERE book_pic_s IS NOT NULL AND book_pic_s != ''"
+ rows, err := bc.db.Query(query)
+ if err != nil {
+ log.Printf("查询执行失败: %v", err)
+ c.JSON(500, gin.H{"error": "数据库查询失败"})
+ return
+ }
+ defer rows.Close()
+
+ // 创建Excel文件
+ f := excelize.NewFile()
+ defer func() {
+ if err := f.Close(); err != nil {
+ log.Printf("关闭Excel文件时出错: %v", err)
+ }
+ }()
+
+ // 创建工作表
+ sheetName := "ISBN数据"
+ index, err := f.NewSheet(sheetName)
+ if err != nil {
+ log.Printf("创建工作表失败: %v", err)
+ c.JSON(500, gin.H{"error": "创建Excel工作表失败"})
+ return
+ }
+
+ // 设置表头样式
+ headerStyle, _ := f.NewStyle(&excelize.Style{
+ Font: &excelize.Font{Bold: true, Color: "#FFFFFF"},
+ Fill: excelize.Fill{Type: "pattern", Color: []string{"#4F81BD"}, Pattern: 1},
+ Alignment: &excelize.Alignment{Horizontal: "center"},
+ })
+ f.SetCellStyle(sheetName, "A1", "A1", headerStyle)
+
+ // 设置表头
+ f.SetCellValue(sheetName, "A1", "ISBN")
+
+ // 设置列宽
+ f.SetColWidth(sheetName, "A", "A", 25)
+
+ // 写入数据(最多5万条)
+ rowNum := 2
+ var count int
+ maxRecords := 50000
+
+ for rows.Next() {
+ var isbn string
+ if err := rows.Scan(&isbn); err != nil {
+ log.Printf("扫描行数据失败: %v", err)
+ continue
+ }
+
+ f.SetCellValue(sheetName, fmt.Sprintf("A%d", rowNum), isbn)
+ rowNum++
+ count++
+
+ // 进度显示
+ if count%1000 == 0 {
+ log.Printf("已处理 %d 条ISBN...", count)
+ }
+
+ // 达到最大限制时停止
+ if count >= maxRecords {
+ break
+ }
+ }
+
+ // 设置活动工作表
+ f.SetActiveSheet(index)
+
+ // 生成文件名
+ fileName := fmt.Sprintf("C:\\Users\\pc\\Desktop\\book_isbns_%s_%d条.xlsx",
+ time.Now().Format("20060102_150477"), count)
+
+ // 保存Excel文件
+ if err := f.SaveAs(fileName); err != nil {
+ log.Printf("保存Excel文件失败: %v", err)
+ c.JSON(500, gin.H{"error": "保存Excel文件失败"})
+ return
+ }
+
+ log.Printf("成功导出 %d 条ISBN到 %s", count, fileName)
+ c.JSON(200, gin.H{
+ "message": "ISBN导出成功",
+ "count": count,
+ "filePath": fileName,
+ })
+}
+
+// func (bc *BookCenterController) updateBooks(c *gin.Context) {
+// // 1. 连接到目标数据库
+// targetDSN := "book:xTxZDK3fxri8pNNW@tcp(118.195.145.133:3306)/book?charset=utf8mb4&parseTime=True&loc=Local"
+// db, err := sql.Open("mysql", targetDSN)
+// if err != nil {
+// c.JSON(500, gin.H{"error": "数据库连接失败"})
+// return
+// }
+// defer db.Close()
+// db.SetMaxOpenConns(10)
+//
+// // 2. 查询书名和ISBN
+// rows, err := db.Query("SELECT id, book_name, isbn FROM book_center")
+// if err != nil {
+// c.JSON(500, gin.H{"error": "查询失败"})
+// return
+// }
+// defer rows.Close()
+//
+// const basePath = "/file/goods_img"
+// var success, fail int
+//
+// // 创建Redis管道批量操作
+// pipe := bc.redis.Pipeline()
+// defer pipe.Close()
+//
+// for rows.Next() {
+// var id int
+// var name, isbn string
+// if err := rows.Scan(&id, &name, &isbn); err != nil {
+// log.Printf("Error scanning row: %v", err)
+// fail++
+// continue
+// }
+//
+// // 3. 通过MD5获取首个字符
+// hash := md5.Sum([]byte(name))
+// firstChar := hex.EncodeToString(hash[:])[0:1]
+// imageFile := fmt.Sprintf("%s_01.jpg", isbn)
+//
+// // 4. 使用filepath构造路径
+// localFilePath := filepath.Join(basePath, firstChar, imageFile)
+//
+// // 5. 检查文件是否存在
+// if _, err := os.Stat(localFilePath); err != nil {
+// log.Printf("File not found for book %s (ISBN: %s) at path: %s", name, isbn, localFilePath)
+// fail++
+// continue
+// }
+//
+// imageFileUrl := fmt.Sprintf("https://book.center.image.buzhiyushu.cn/%s/%s", firstChar, imageFile)
+//
+// // 6. 开启事务处理MySQL和Redis
+// tx, err := db.Begin()
+// if err != nil {
+// log.Printf("Error starting transaction for book %s: %v", name, err)
+// fail++
+// continue
+// }
+//
+// // 更新MySQL
+// if _, err = tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id); err != nil {
+// tx.Rollback()
+// log.Printf("Error updating book_pic for book %s: %v", name, err)
+// fail++
+// continue
+// }
+// //
+// // 更新Redis
+// fmt.Sprintf("isbn:%s book_pic:%s", isbn, imageFileUrl)
+// // 提交事务
+// if err = tx.Commit(); err != nil {
+// bc.sendErrorResponse(c, http.StatusInternalServerError, 500, "事务提交失败", err.Error())
+// return
+// }
+//
+// success++
+// log.Printf("Updated book_pic for: %s (ISBN: %s)", name, isbn)
+// }
+//
+// if err = rows.Err(); err != nil {
+// c.JSON(500, gin.H{"error": "遍历结果集失败"})
+// return
+// }
+//
+// c.JSON(200, gin.H{
+// "message": "更新完成",
+// "success": success,
+// "fail": fail,
+// })
+// }
+//func (bc *BookCenterController) updateBooks(c *gin.Context) {
+//
+// // 1. 从ApiFox获取起始页参数
+// startPage, err := strconv.Atoi(c.DefaultQuery("startPage", "1"))
+// if err != nil || startPage < 1 {
+// startPage = 1
+// }
+// const pageSize = 100 // 固定每页100条数据
+//
+// // 1. 连接到目标MySQL数据库
+// targetDSN := "book:xTxZDK3fxri8pNNW@tcp(118.195.145.133:3306)/book?charset=utf8mb4&parseTime=True&loc=Local"
+// db, err := sql.Open("mysql", targetDSN)
+// if err != nil {
+// c.JSON(500, gin.H{"error": "数据库连接失败"})
+// return
+// }
+// defer db.Close()
+// db.SetMaxOpenConns(10)
+// // 4. 自动分页处理循环
+// for page := startPage; ; page++ {
+// offset := (page - 1) * pageSize
+// log.Printf("正在处理第 %d 页,偏移量: %d", page, offset)
+// // 2. 查询书籍完整信息
+// rows, err := db.Query(`SELECT
+// id, category, book_name, book_pic,
+// COALESCE(book_pic_s, '{}') AS book_pic_s, -- 如果 NULL,返回 '{}'
+// isbn, author, editor, binding_layout, publisher,
+// edition, format, languages,
+// COALESCE(publication_time, 0) AS publication_time, -- 如果 NULL,返回 0
+//COALESCE(print_time, 0) AS print_time,
+// COALESCE(paper, '') AS paper,
+// COALESCE(pages, 0) AS pages,
+// COALESCE(wordage, '') AS wordage,
+// COALESCE(fix_price, 0) AS fix_price,
+// COALESCE(content, '') AS content,
+// COALESCE(remark, '') AS remark,
+// COALESCE(vio_book, 0) AS vio_book,
+// COALESCE(book_set, 0) AS book_set,
+// COALESCE(onenum_mbooks, 0) AS onenum_mbooks,
+// COALESCE(ill_publisher, 0) AS ill_publisher,
+// COALESCE(ill_author, 0) AS ill_author,
+// COALESCE(day_sale_7, 0) AS day_sale_7,
+// COALESCE(day_sale_15, 0) AS day_sale_15,
+// COALESCE(day_sale_30, 0) AS day_sale_30,
+// COALESCE(day_sale_60, 0) AS day_sale_60,
+// COALESCE(day_sale_90, 0) AS day_sale_90,
+// COALESCE(day_sale_180, 0) AS day_sale_180,
+// COALESCE(day_sale_365, 0) AS day_sale_365,
+// COALESCE(this_year_sale, 0) AS this_year_sale,
+// COALESCE(last_year_sale, 0) AS last_year_sale,
+// COALESCE(total_sale, 0) AS total_sale,
+// COALESCE(sold_out_times, '') AS sold_out_times,
+// COALESCE(shipment_cycle, 0) AS shipment_cycle,
+// COALESCE(buy_count, '') AS buy_count,
+// COALESCE(sell_count, '') AS sell_count,
+// COALESCE(publiction_times, 0) AS publiction_times,
+// COALESCE(cat_id, 0) AS cat_id,
+// COALESCE(buy_counts, 0) AS buy_counts,
+// COALESCE(sell_counts, 0) AS sell_counts,
+// COALESCE(create_by, 0) AS create_by,
+// COALESCE(create_time, 0) AS create_time,
+// COALESCE(update_by, 0) AS update_by,
+// COALESCE(update_time, 0) AS update_time
+// FROM book_center LIMIT ? OFFSET ?`, pageSize, offset)
+// if err != nil {
+// c.JSON(500, gin.H{"error": "查询失败"})
+// return
+// }
+// defer rows.Close()
+//
+// const basePath = "/file/goods_img"
+// var success, fail int
+//
+// // 获取列名信息
+// columns, err := rows.Columns()
+// if err != nil {
+// c.JSON(500, gin.H{"error": "获取列名失败"})
+// return
+// }
+//
+// // 准备值容器
+// values := make([]interface{}, len(columns))
+// valuePtrs := make([]interface{}, len(columns))
+// for i := range columns {
+// valuePtrs[i] = &values[i]
+// }
+//
+// for rows.Next() {
+// // 扫描行数据
+// if err := rows.Scan(valuePtrs...); err != nil {
+// log.Printf("扫描行数据失败: %v", err)
+// fail++
+// continue
+// }
+//
+// // 将行数据转换为map
+// bookData := make(map[string]interface{})
+// for i, col := range columns {
+// val := values[i]
+// b, ok := val.([]byte)
+// if ok {
+// bookData[col] = string(b)
+// } else {
+// bookData[col] = val
+// }
+// }
+//
+// // 获取书名和ISBN
+// name, _ := bookData["book_name"].(string)
+// isbn, _ := bookData["isbn"].(string)
+// //id, _ := bookData["id"].(int64)
+// var id int64
+// switch v := bookData["id"].(type) {
+// case int64:
+// id = v
+// case uint64:
+// id = int64(v)
+// case int32:
+// id = int64(v)
+// case uint32:
+// id = int64(v)
+// case int:
+// id = int64(v)
+// case uint:
+// id = int64(v)
+// case []byte:
+// id, _ = strconv.ParseInt(string(v), 10, 64)
+// case string:
+// id, _ = strconv.ParseInt(v, 10, 64)
+// default:
+// log.Printf("无法解析 ID 类型: %T,值: %v", v, v)
+// fail++
+// continue
+// }
+//
+// // 3. 通过MD5获取首个字符
+// hash := md5.Sum([]byte(name))
+// firstChar := hex.EncodeToString(hash[:])[0:1]
+// imageFile := fmt.Sprintf("%s_01.jpg", isbn)
+//
+// // 4. 构造本地文件路径
+// localFilePath := filepath.Join(basePath, firstChar, imageFile)
+//
+// // 5. 检查文件是否存在
+// if _, err := os.Stat(localFilePath); err != nil {
+// log.Printf("书籍图片未找到: %s (ISBN: %s) 路径: %s", name, isbn, localFilePath)
+// fail++
+// continue
+// }
+//
+// // 构造图片URL
+// imageFileUrl := fmt.Sprintf("https://book.center.image.buzhiyushu.cn/%s/%s", firstChar, imageFile)
+// bookData["book_pic"] = imageFileUrl
+// // 6. 开启事务处理MySQL和Redis
+// tx, err := db.Begin()
+// if err != nil {
+// log.Printf("事务开启失败: %s: %v", name, err)
+// fail++
+// continue
+// }
+//
+// // 修改点1: 添加MySQL更新日志
+// log.Printf("准备更新MySQL记录 ID: %d, ISBN: %s, 新图片URL: %s", id, isbn, imageFileUrl)
+//
+// // 更新MySQL中的book_pic字段
+// result, err := tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id)
+// if err != nil {
+// tx.Rollback()
+// log.Printf("更新book_pic失败: %s: %v", name, err)
+// fail++
+// continue
+// }
+//
+// // 修改点2: 检查实际影响的行数
+// rowsAffected, _ := result.RowsAffected()
+// log.Printf("MySQL更新影响行数: %d (ID: %d)", rowsAffected, id)
+//
+// // 将书籍数据转换为JSON格式
+// bookJSON, err := json.Marshal(bookData)
+// if err != nil {
+// tx.Rollback()
+// log.Printf("JSON序列化失败: %s: %v", name, err)
+// fail++
+// continue
+// }
+//
+// // 修改点3: 添加Redis操作日志便于调试
+// log.Printf("准备更新Redis键: %s 数据: %s", isbn, string(bookJSON))
+//
+// // 更新Redis
+// err = bc.redis.Set(context.Background(), isbn, bookJSON, 0).Err()
+// if err != nil {
+// tx.Rollback()
+// log.Printf("Redis更新失败: %s: %v", name, err)
+// fail++
+// continue
+// }
+//
+// // 修改点4: 添加事务提交日志
+// log.Printf("准备提交事务 (ID: %d, ISBN: %s)", id, isbn)
+// if err := tx.Commit(); err != nil {
+// log.Printf("事务提交失败: %s: %v", name, err)
+// fail++
+// continue
+// }
+//
+// success++
+// log.Printf("更新成功: %s (ISBN: %s)", name, isbn)
+//
+// //// 6. 开启事务处理MySQL和Redis
+// //tx, err := db.Begin()
+// //if err != nil {
+// // log.Printf("事务开启失败: %s: %v", name, err)
+// // fail++
+// // continue
+// //}
+// //
+// //// 更新MySQL中的book_pic字段
+// //if _, err = tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id); err != nil {
+// // tx.Rollback()
+// // log.Printf("更新book_pic失败: %s: %v", name, err)
+// // fail++
+// // continue
+// //}
+// //
+// //// 将书籍数据转换为JSON格式
+// //bookJSON, err := json.Marshal(bookData)
+// //if err != nil {
+// // tx.Rollback()
+// // log.Printf("JSON序列化失败: %s: %v", name, err)
+// // fail++
+// // continue
+// //}
+// //
+// //// 修改点4:添加Redis操作日志便于调试
+// //log.Printf("Updating Redis key: %s with data: %s", isbn, string(bookJSON))
+// //
+// //// 更新Redis - 现在会在第一次请求时就执行
+// //err = bc.redis.Set(context.Background(), isbn, bookJSON, 0).Err()
+// //if err != nil {
+// // tx.Rollback()
+// // c.JSON(http.StatusInternalServerError, gin.H{"error": "Redis操作失败: " + err.Error()})
+// // return
+// //}
+// //
+// //// 6. 提交事务
+// //if err := tx.Commit(); err != nil {
+// // c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库事务提交失败: " + err.Error()})
+// // return
+// //}
+// //
+// //success++
+// //log.Printf("更新成功: %s (ISBN: %s)", name, isbn)
+// }
+//
+// if err = rows.Err(); err != nil {
+// c.JSON(500, gin.H{"error": "遍历结果集失败"})
+// return
+// }
+// }
+//
+// c.JSON(200, gin.H{
+// "message": "更新完成",
+// "success": success,
+// "fail": fail,
+// })
+//}
+
+func (bc *BookCenterController) updateBooks(c *gin.Context) {
+ // 1. 从ApiFox获取起始页参数
+ startPage, err := strconv.Atoi(c.DefaultQuery("startPage", "1"))
+ if err != nil || startPage < 1 {
+ startPage = 1
+ }
+ const pageSize = 100 // 固定每页100条数据
+
+ // 2. 连接到目标MySQL数据库
+ targetDSN := "book:xTxZDK3fxri8pNNW@tcp(118.195.145.133:3306)/book?charset=utf8mb4&parseTime=True&loc=Local"
+ db, err := sql.Open("mysql", targetDSN)
+ if err != nil {
+ c.JSON(500, gin.H{"error": "数据库连接失败"})
+ return
+ }
+ defer db.Close()
+ db.SetMaxOpenConns(10)
+
+ // 3. 初始化统计变量
+ var globalSuccess, globalFail int
+ const basePath = "/file/goods_img"
+
+ // 4. 自动分页处理循环
+ for page := startPage; ; page++ {
+ offset := (page - 1) * pageSize
+ log.Printf("正在处理第 %d 页,偏移量: %d", page, offset)
+
+ // 5. 分页查询书籍信息
+ rows, err := db.Query(`SELECT
+ id, category, book_name, book_pic,
+ COALESCE(book_pic_s, '""""') AS book_pic_s,
+ isbn, author, editor, binding_layout, publisher,
+ edition, format, languages,
+ COALESCE(publication_time, 0) AS publication_time,
+ COALESCE(print_time, 0) AS print_time,
+ COALESCE(paper, '') AS paper,
+ COALESCE(pages, 0) AS pages,
+ COALESCE(wordage, '') AS wordage,
+ COALESCE(fix_price, 0) AS fix_price,
+ COALESCE(content, '') AS content,
+ COALESCE(remark, '') AS remark,
+ COALESCE(vio_book, 0) AS vio_book,
+ COALESCE(book_set, 0) AS book_set,
+ COALESCE(onenum_mbooks, 0) AS onenum_mbooks,
+ COALESCE(ill_publisher, 0) AS ill_publisher,
+ COALESCE(ill_author, 0) AS ill_author,
+ COALESCE(day_sale_7, 0) AS day_sale_7,
+ COALESCE(day_sale_15, 0) AS day_sale_15,
+ COALESCE(day_sale_30, 0) AS day_sale_30,
+ COALESCE(day_sale_60, 0) AS day_sale_60,
+ COALESCE(day_sale_90, 0) AS day_sale_90,
+ COALESCE(day_sale_180, 0) AS day_sale_180,
+ COALESCE(day_sale_365, 0) AS day_sale_365,
+ COALESCE(this_year_sale, 0) AS this_year_sale,
+ COALESCE(last_year_sale, 0) AS last_year_sale,
+ COALESCE(total_sale, 0) AS total_sale,
+ COALESCE(sold_out_times, '') AS sold_out_times,
+ COALESCE(shipment_cycle, 0) AS shipment_cycle,
+ COALESCE(buy_counts, '') AS buy_count,
+ COALESCE(sell_counts, '') AS sell_count,
+ COALESCE(publiction_times, 0) AS publiction_times,
+ COALESCE(cat_id, 0) AS cat_id,
+ COALESCE(buy_counts, 0) AS buy_counts,
+ COALESCE(sell_counts, 0) AS sell_counts,
+ COALESCE(create_by, 0) AS create_by,
+ COALESCE(create_time, 0) AS create_time,
+ COALESCE(update_by, 0) AS update_by,
+ COALESCE(update_time, 0) AS update_time
+ FROM book_center LIMIT ? OFFSET ?`, pageSize, offset)
+
+ if err != nil {
+ log.Printf("第 %d 页查询失败: %v", page, err)
+ break
+ }
+
+ // 6. 处理当前页数据
+ var pageSuccess, pageFail int
+ columns, err := rows.Columns()
+ if err != nil {
+ rows.Close()
+ log.Printf("获取列名失败: %v", err)
+ break
+ }
+
+ values := make([]interface{}, len(columns))
+ valuePtrs := make([]interface{}, len(columns))
+ for i := range columns {
+ valuePtrs[i] = &values[i]
+ }
+
+ hasData := false
+ for rows.Next() {
+ hasData = true
+
+ // 扫描行数据
+ if err := rows.Scan(valuePtrs...); err != nil {
+ log.Printf("扫描行数据失败: %v", err)
+ pageFail++
+ continue
+ }
+
+ // 将行数据转换为map
+ bookData := make(map[string]interface{})
+ for i, col := range columns {
+ val := values[i]
+ b, ok := val.([]byte)
+ if ok {
+ bookData[col] = string(b)
+ } else {
+ bookData[col] = val
+ }
+ }
+
+ // 获取书名和ISBN
+ name, _ := bookData["book_name"].(string)
+ isbn, _ := bookData["isbn"].(string)
+ var id int64
+ switch v := bookData["id"].(type) {
+ case int64:
+ id = v
+ case uint64:
+ id = int64(v)
+ case int32:
+ id = int64(v)
+ case uint32:
+ id = int64(v)
+ case int:
+ id = int64(v)
+ case uint:
+ id = int64(v)
+ case []byte:
+ id, _ = strconv.ParseInt(string(v), 10, 64)
+ case string:
+ id, _ = strconv.ParseInt(v, 10, 64)
+ default:
+ log.Printf("无法解析 ID 类型: %T,值: %v", v, v)
+ pageFail++
+ continue
+ }
+
+ // 处理图片逻辑
+ hash := md5.Sum([]byte(name))
+ firstChar := hex.EncodeToString(hash[:])[0:1]
+ imageFile := fmt.Sprintf("%s_01.jpg", isbn)
+ localFilePath := filepath.Join(basePath, firstChar, imageFile)
+
+ if _, err := os.Stat(localFilePath); err != nil {
+ log.Printf("书籍图片未找到: %s (ISBN: %s) 路径: %s", name, isbn, localFilePath)
+ pageFail++
+ continue
+ }
+
+ imageFileUrl := fmt.Sprintf("https://book.center.image.buzhiyushu.cn/%s/%s", firstChar, imageFile)
+ bookData["book_pic"] = imageFileUrl
+
+ // 事务处理
+ tx, err := db.Begin()
+ if err != nil {
+ log.Printf("事务开启失败: %s: %v", name, err)
+ pageFail++
+ continue
+ }
+
+ // 更新MySQL
+ _, err = tx.Exec("UPDATE book_center SET book_pic = ? WHERE id = ?", imageFileUrl, id)
+ if err != nil {
+ tx.Rollback()
+ log.Printf("更新book_pic失败: %s: %v", name, err)
+ pageFail++
+ continue
+ }
+
+ // 更新Redis
+ bookJSON, err := json.Marshal(bookData)
+ if err != nil {
+ tx.Rollback()
+ log.Printf("JSON序列化失败: %s: %v", name, err)
+ pageFail++
+ continue
+ }
+
+ err = bc.redis.Set(context.Background(), isbn, bookJSON, 0).Err()
+ if err != nil {
+ tx.Rollback()
+ log.Printf("Redis更新失败: %s: %v", name, err)
+ pageFail++
+ continue
+ }
+
+ if err := tx.Commit(); err != nil {
+ log.Printf("事务提交失败: %s: %v", name, err)
+ pageFail++
+ continue
+ }
+
+ pageSuccess++
+ log.Printf("更新成功: %s (ISBN: %s)", name, isbn)
+ }
+
+ rows.Close()
+ if err = rows.Err(); err != nil {
+ log.Printf("遍历结果集失败: %v", err)
+ }
+
+ globalSuccess += pageSuccess
+ globalFail += pageFail
+ log.Printf("第 %d 页处理完成: 成功 %d, 失败 %d", page, pageSuccess, pageFail)
+
+ // 7. 如果没有数据,结束循环
+ if !hasData {
+ log.Printf("第 %d 页无数据,处理结束", page)
+ break
+ }
+ }
+
+ // 8. 返回最终结果
+ c.JSON(200, gin.H{
+ "message": "批量更新完成",
+ "success": globalSuccess,
+ "fail": globalFail,
+ "processedFromPage": startPage,
+ "timestamp": time.Now().Format(time.RFC3339),
+ })
+}
+
+// GetBookPicByISBN 根据ISBN获取书籍小图
+func (bc *BookCenterController) GetBookPicByISBN(c *gin.Context) {
+ // 获取ISBN参数
+ isbn := c.Query("isbn")
+ if isbn == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN参数不能为空"})
+ return
+ }
+
+ // 验证ISBN格式
+ if !isValidISBN(isbn) {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN格式不正确"})
+ return
+ }
+
+ // 查询数据库中的book_pic_s字段
+ var bookPicS sql.NullString
+ query := "SELECT book_pic_s FROM book_center WHERE isbn = ?"
+
+ err := bc.db.QueryRow(query, isbn).Scan(&bookPicS)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ c.JSON(http.StatusNotFound, gin.H{"error": "未找到该ISBN对应的图书"})
+ return
+ }
+ log.Printf("数据库查询失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败"})
+ return
+ }
+
+ // 如果book_pic_s为空或null,返回空字符串
+ if !bookPicS.Valid || bookPicS.String == "" {
+ c.JSON(http.StatusOK, gin.H{"localPath": ""})
+ return
+ }
+
+ // 解析JSON数据
+ var picData map[string]interface{}
+ if err := json.Unmarshal([]byte(bookPicS.String), &picData); err != nil {
+ log.Printf("解析book_pic_s JSON失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "图片数据格式错误"})
+ return
+ }
+
+ // 提取localPath字段
+ localPath := ""
+ if path, exists := picData["localPath"]; exists {
+ if pathStr, ok := path.(string); ok {
+ localPath = pathStr
+ }
+ }
+
+ // 返回localPath值
+ c.JSON(http.StatusOK, gin.H{"localPath": localPath})
+}
+
+// parseISBNs 解析ISBNs参数
+func (bc *BookCenterController) parseISBNs(isbns interface{}) ([]string, error) {
+ var result []string
+
+ switch v := isbns.(type) {
+ case string:
+ if v == "" {
+ return nil, fmt.Errorf("必备参数为空isbns")
+ }
+ strISBNs := strings.Split(v, ",")
+ for _, strISBN := range strISBNs {
+ strISBN = strings.TrimSpace(strISBN)
+ if strISBN == "" {
+ continue
+ }
+ // 验证ISBN格式
+ if !isValidISBN(strISBN) {
+ return nil, fmt.Errorf("ISBN格式错误: %s", strISBN)
+ }
+ result = append(result, strISBN)
+ }
+ case []interface{}:
+ if len(v) == 0 {
+ return nil, fmt.Errorf("必备参数为空isbns")
+ }
+ for _, isbn := range v {
+ switch isbnVal := isbn.(type) {
+ case string:
+ isbnVal = strings.TrimSpace(isbnVal)
+ if isbnVal == "" {
+ continue
+ }
+ // 验证ISBN格式
+ if !isValidISBN(isbnVal) {
+ return nil, fmt.Errorf("ISBN格式错误: %s", isbnVal)
+ }
+ result = append(result, isbnVal)
+ default:
+ return nil, fmt.Errorf("ISBN类型错误,必须是字符串")
+ }
+ }
+ default:
+ return nil, fmt.Errorf("ISBN类型错误,必须是字符串或数组")
+ }
+
+ if len(result) == 0 {
+ return nil, fmt.Errorf("未提供有效的ISBN")
+ }
+
+ return result, nil
+}
+
+// prepareUpdateDataByISBN 准备要更新的数据(根据ISBN)
+func (bc *BookCenterController) prepareUpdateDataByISBN(request SetBookBaseInfoToIllByISBNRequest) map[string]interface{} {
+ updateData := make(map[string]interface{})
+
+ if request.VioBook != nil {
+ updateData["vio_book"] = *request.VioBook
+ }
+ if request.BookSet != nil {
+ updateData["book_set"] = *request.BookSet
+ }
+ if request.OnenumMbooks != nil {
+ updateData["onenum_mbooks"] = *request.OnenumMbooks
+ }
+ if request.IllPublisher != nil {
+ updateData["ill_publisher"] = *request.IllPublisher
+ }
+ if request.IllAuthor != nil {
+ updateData["ill_author"] = *request.IllAuthor
+ }
+
+ return updateData
+}
+
+// updateMySQLByISBN 根据ISBN更新MySQL数据库
+func (bc *BookCenterController) updateMySQLByISBN(tx *sql.Tx, isbns []string, updateData map[string]interface{}) (int64, error) {
+ log.Printf("updateMySQLByISBN: 开始构建SQL查询,ISBNs: %v, updateData: %+v", isbns, updateData)
+
+ // 构建SQL查询
+ query := "UPDATE book_center SET "
+ var setParts []string
+ var args []interface{}
+
+ for field, value := range updateData {
+ setParts = append(setParts, field+" = ?")
+ args = append(args, value)
+ log.Printf("updateMySQLByISBN: 添加更新字段: %s = %v", field, value)
+ }
+ query += strings.Join(setParts, ", ") + " WHERE isbn IN ("
+
+ // 添加ISBN占位符
+ placeholders := make([]string, len(isbns))
+ for i := range isbns {
+ placeholders[i] = "?"
+ args = append(args, isbns[i])
+ }
+ query += strings.Join(placeholders, ",") + ")"
+
+ log.Printf("updateMySQLByISBN: 构建的SQL查询: %s", query)
+ log.Printf("updateMySQLByISBN: SQL参数: %v", args)
+
+ // 执行MySQL更新
+ log.Printf("updateMySQLByISBN: 开始执行SQL更新")
+ result, err := tx.Exec(query, args...)
+ if err != nil {
+ log.Printf("updateMySQLByISBN: ❌ SQL执行失败: %v", err)
+ return 0, fmt.Errorf("数据库更新错误: %v", err)
+ }
+ log.Printf("updateMySQLByISBN: ✅ SQL执行成功")
+
+ // 检查影响的行数
+ rowsAffected, err := result.RowsAffected()
+ if err != nil {
+ log.Printf("updateMySQLByISBN: ❌ 获取影响行数失败: %v", err)
+ return 0, fmt.Errorf("获取影响行数错误: %v", err)
+ }
+
+ log.Printf("updateMySQLByISBN: ✅ 更新完成,影响行数: %d", rowsAffected)
+ return rowsAffected, nil
+}
+
+// UploadBookPic 上传图书图片并更新book_pic_new字段
+// UploadBookPic 上传图书图片并更新book_pic_new字段(支持文件上传和网络URL)
+// UploadBookPic 上传图书图片并更新book_pic_new字段(支持网络URL)【JSON方式】
+// UploadBookPic 仅保存图书图片URL到 book_pic_new 字段(结构仅包含 pddPath)
+func (bc *BookCenterController) UploadBookPic(c *gin.Context) {
+ type UploadRequest struct {
+ Isbn string `json:"isbn"`
+ File string `json:"file"` // 实际上传的是URL
+ }
+
+ var req UploadRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "请求参数解析失败",
+ "error": err.Error(),
+ })
+ return
+ }
+
+ isbn := req.Isbn
+ fileURL := req.File
+
+ if isbn == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "ISBN参数不能为空",
+ })
+ return
+ }
+ if !isValidISBN(isbn) {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "ISBN格式不正确",
+ })
+ return
+ }
+ if fileURL == "" || !isValidURL(fileURL) || !isImageURL(fileURL) {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "success": false,
+ "message": "请提供有效的图片URL",
+ })
+ return
+ }
+
+ // 检查图书是否存在
+ var bookID int
+ err := bc.db.QueryRow("SELECT id FROM book_center WHERE isbn = ? LIMIT 1", isbn).Scan(&bookID)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ c.JSON(http.StatusNotFound, gin.H{
+ "success": false,
+ "message": "未找到该ISBN对应的图书",
+ })
+ } else {
+ log.Printf("数据库查询失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "success": false,
+ "message": "数据库查询失败",
+ })
+ }
+ return
+ }
+
+ // 构造 JSON 结构,仅包含 pddPath 字段
+ bookPicNew := map[string]string{
+ "pddPath": fileURL,
+ }
+ bookPicNewJSON, err := json.Marshal(bookPicNew)
+ if err != nil {
+ log.Printf("序列化 book_pic_new 失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "success": false,
+ "message": "数据序列化失败",
+ })
+ return
+ }
+
+ // 写入数据库
+ tx, err := bc.db.Begin()
+ if err != nil {
+ log.Printf("开启事务失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "success": false,
+ "message": "开启事务失败",
+ })
+ return
+ }
+
+ updateQuery := "UPDATE book_center SET book_pic_new = ?, update_time = ? WHERE isbn = ?"
+ result, err := tx.Exec(updateQuery, string(bookPicNewJSON), time.Now().Unix(), isbn)
+ if err != nil {
+ tx.Rollback()
+ log.Printf("更新数据库失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "success": false,
+ "message": "更新数据库失败",
+ })
+ return
+ }
+
+ rowsAffected, err := result.RowsAffected()
+ if err != nil || rowsAffected == 0 {
+ tx.Rollback()
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "success": false,
+ "message": "更新失败或未找到记录",
+ })
+ return
+ }
+
+ if err := tx.Commit(); err != nil {
+ log.Printf("提交事务失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "success": false,
+ "message": "提交事务失败",
+ })
+ return
+ }
+
+ // 异步更新 Redis 缓存(可选)
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Printf("更新Redis缓存发生panic: %v", r)
+ }
+ }()
+
+ redisKey := isbn
+ bookData, err := bc.redis.Get(context.Background(), redisKey).Result()
+ if err == nil {
+ var result map[string]interface{}
+ if err := json.Unmarshal([]byte(bookData), &result); err == nil {
+ result["book_pic_new"] = bookPicNew
+ updatedData, _ := json.Marshal(result)
+ bc.redis.Set(context.Background(), redisKey, updatedData, 0)
+ }
+ }
+ }()
+
+ // 返回成功响应
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "图片URL保存成功",
+ "data": gin.H{
+ "isbn": isbn,
+ "book_pic_new": bookPicNew,
+ },
+ })
+}
+
+func (bc *BookCenterController) getBookByIsbnXcx(c *gin.Context) {
+ // 解析请求参数
+ var request struct {
+ BuyCount int `json:"buyCount"`
+ Price float64 `json:"price"`
+ Author string `json:"author"`
+ BookPicNew string `json:"book_pic_new"`
+ ISBN string `json:"isbn"`
+ SellCount int `json:"sellCount"`
+ Publisher string `json:"publisher"`
+ BookName string `json:"book_name"`
+ }
+
+ if err := c.ShouldBindJSON(&request); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "参数解析错误", "message": err.Error()})
+ return
+ }
+
+ // 验证ISBN格式
+ if request.ISBN == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ISBN不能为空"})
+ return
+ }
+
+ // 1. 先尝试从Redis获取数据
+ redisKey := request.ISBN
+ bookData, err := bc.redis.Get(context.Background(), redisKey).Result()
+
+ if err == nil {
+ // Redis中存在数据,解析并检查是否需要更新
+ var bookInfo map[string]interface{}
+ if err := json.Unmarshal([]byte(bookData), &bookInfo); err == nil {
+ // 检查price, author, publisher是否有值
+ needUpdate := false
+
+ // 检查price字段
+ if price, exists := bookInfo["fix_price"]; !exists || price == nil || price == "" || price == 0 {
+ bookInfo["fix_price"] = request.Price
+ needUpdate = true
+ }
+
+ // 检查author字段
+ if author, exists := bookInfo["author"]; !exists || author == nil || author == "" {
+ bookInfo["author"] = request.Author
+ needUpdate = true
+ }
+
+ // 检查publisher字段
+ if publisher, exists := bookInfo["publisher"]; !exists || publisher == nil || publisher == "" {
+ bookInfo["publisher"] = request.Publisher
+ needUpdate = true
+ }
+
+ // 如果需要更新,更新Redis和数据库
+ if needUpdate {
+ // 更新Redis
+ updatedData, err := json.Marshal(bookInfo)
+ if err == nil {
+ bc.redis.Set(context.Background(), redisKey, updatedData, 0)
+ }
+
+ // 更新数据库
+ query := `UPDATE book_center SET fix_price = ?, author = ?, publisher = ? WHERE isbn = ?`
+ _, err = bc.db.Exec(query, request.Price, request.Author, request.Publisher, request.ISBN)
+ if err != nil {
+ log.Printf("更新数据库失败: %v", err)
+ }
+ }
+
+ // 返回Redis中的数据
+ c.JSON(http.StatusOK, gin.H{
+ "data": bookInfo,
+ "source": "redis",
+ })
+ return
+ }
+ }
+
+ // 2. Redis中没有数据,尝试从数据库获取
+ query := `SELECT
+ id, book_name, book_pic, book_pic_s, book_pic_new, isbn, author, category, publisher,
+ publication_time, binding_layout, fix_price, buy_counts, sell_counts
+ FROM book_center
+ WHERE isbn = ?`
+
+ var (
+ id, bookName, bookPic, dbIsbn, author, category, publisher string
+ bindingLayout sql.NullString
+ bookPicS, bookPicNew sql.NullString
+ buyCount, sellCount sql.NullString
+ publicationTime interface{}
+ fixPrice int64
+ )
+
+ err = bc.db.QueryRow(query, request.ISBN).Scan(
+ &id, &bookName, &bookPic, &bookPicS, &bookPicNew, &dbIsbn, &author, &category, &publisher,
+ &publicationTime, &bindingLayout, &fixPrice, &buyCount, &sellCount,
+ )
+
+ if err == nil {
+ // 数据库中存在数据,构建响应并缓存到Redis
+ bookInfo := map[string]interface{}{
+ "id": id,
+ "book_name": bookName,
+ "book_pic": bookPic,
+ "book_pic_s": bookPicS.String,
+ "book_pic_new": bookPicNew.String,
+ "isbn": dbIsbn,
+ "author": author,
+ "category": category,
+ "publisher": publisher,
+ "publication_time": publicationTime,
+ "binding_layout": bindingLayout.String,
+ "fix_price": fixPrice,
+ "buy_counts": buyCount.String,
+ "sell_counts": sellCount.String,
+ }
+
+ // 缓存到Redis
+ if jsonData, err := json.Marshal(bookInfo); err == nil {
+ bc.redis.Set(context.Background(), redisKey, jsonData, 0)
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "data": bookInfo,
+ "source": "database",
+ })
+ return
+ } else if err == sql.ErrNoRows {
+ // 3. 数据库中也没有数据,插入新数据
+ insertQuery := `INSERT INTO book_center (
+ book_name, isbn, author, publisher, fix_price, book_pic_new, total_sale,create_time, update_time
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+
+ now := time.Now().Unix()
+ _, err = bc.db.Exec(insertQuery,
+ request.BookName,
+ request.ISBN,
+ request.Author,
+ request.Publisher,
+ request.Price,
+ request.BookPicNew,
+ request.BuyCount,
+ now,
+ now,
+ )
+
+ if err != nil {
+ log.Printf("插入数据失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "插入数据失败", "message": err.Error()})
+ return
+ }
+
+ // 构建新数据并缓存到Redis
+ newBookInfo := map[string]interface{}{
+ "book_name": request.BookName,
+ "isbn": request.ISBN,
+ "author": request.Author,
+ "publisher": request.Publisher,
+ "fix_price": request.Price,
+ "buy_counts": request.BuyCount,
+ "sell_counts": request.SellCount,
+ "book_pic_new": request.BookPicNew,
+ "create_time": now,
+ "update_time": now,
+ }
+
+ // 缓存到Redis
+ if jsonData, err := json.Marshal(newBookInfo); err == nil {
+ bc.redis.Set(context.Background(), redisKey, jsonData, 0)
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "data": newBookInfo,
+ "source": "created",
+ })
+ return
+ } else {
+ // 数据库查询出错
+ log.Printf("数据库查询失败: %v", err)
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库查询失败", "message": err.Error()})
+ return
+ }
+}
+
+// isImageFile 检查文件是否为图片类型
+func isImageFile(filename string) bool {
+ ext := strings.ToLower(filepath.Ext(filename))
+ allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
+
+ for _, allowedExt := range allowedExts {
+ if ext == allowedExt {
+ return true
+ }
+ }
+ return false
+} // is
+// ValidURL 检查字符串是否为有效的URL
+func isValidURL(str string) bool {
+ u, err := url.Parse(str)
+ return err == nil && u.Scheme != "" && u.Host != ""
+}
+
+// isImageURL 检查URL是否指向图片文件
+func isImageURL(urlStr string) bool {
+ u, err := url.Parse(urlStr)
+ if err != nil {
+ return false
+ }
+
+ // 获取路径部分的文件扩展名
+ path := u.Path
+ ext := strings.ToLower(filepath.Ext(path))
+ allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
+
+ for _, allowedExt := range allowedExts {
+ if ext == allowedExt {
+ return true
+ }
+ }
+ return false
+}
+
+// UploadBookImage 调用上传接口 (GET 请求)
+func UploadBookImage(imageURL string) (string, error) {
+ // 拼接 URL 参数并进行转义
+ reqURL := fmt.Sprintf("%s/upload?token=%s&imageURL=%s",
+ bookUploadUrl,
+ url.QueryEscape(fixedToken),
+ url.QueryEscape(imageURL),
+ )
+
+ // 发送 GET 请求
+ resp, err := http.Get(reqURL)
+ if err != nil {
+ return "", fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ // 读取响应内容
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("读取响应失败: %v", err)
+ }
+
+ // 检查状态码
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("上传失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
+ }
+
+ return string(body), nil
+}
+
+// processBookImageUpload 处理图书图片上传和字段补全的辅助函数//
+// processBookImageUpload 处理图书图片上传和字段补全的辅助函数
+func (bc *BookCenterController) processBookImageUpload(bookData map[string]interface{}, isbn string) {
+ getString := func(data map[string]interface{}, key string) string {
+ if v, ok := data[key]; ok && v != nil {
+ if str, ok2 := v.(string); ok2 {
+ return str
+ }
+ }
+ return ""
+ }
+
+ needsUpload := false
+ bookPicNewEmpty := false
+ bookPicSEmpty := false
+ needsPddResponseFill := false
+
+ var bookPicNewPddPath string
+ var bookPicNewMap map[string]interface{}
+ var bookPicSMap map[string]interface{}
+
+ // ✅ 兼容字符串/Map 两种格式的 book_pic_new
+ switch v := bookData["book_pic_new"].(type) {
+ case string:
+ if v != "" {
+ json.Unmarshal([]byte(v), &bookPicNewMap)
+ }
+ case map[string]interface{}:
+ bookPicNewMap = v
+ }
+
+ if bookPicNewMap == nil || bookPicNewMap["pddPath"] == nil || bookPicNewMap["pddPath"] == "" {
+ bookPicNewEmpty = true
+ needsUpload = true
+ } else if pathStr, ok := bookPicNewMap["pddPath"].(string); ok && pathStr != "" {
+ bookPicNewPddPath = strings.TrimSpace(pathStr)
+ }
+
+ // ✅ 兼容字符串/Map 两种格式的 book_pic_s
+ switch v := bookData["book_pic_s"].(type) {
+ case string:
+ if v != "" {
+ json.Unmarshal([]byte(v), &bookPicSMap)
+ }
+ case map[string]interface{}:
+ bookPicSMap = v
+ }
+
+ if bookPicSMap == nil || bookPicSMap["pddResponse"] == nil || bookPicSMap["pddResponse"] == "" {
+ bookPicSEmpty = true
+ if bookPicNewPddPath != "" {
+ needsPddResponseFill = true
+ } else {
+ needsUpload = true
+ }
+ }
+
+ // ✅ 如果只需复制 pddResponse
+ if needsPddResponseFill && !needsUpload {
+ log.Printf("📸 ISBN %s 的 book_pic_new 有 pddPath,将其复制到 book_pic_s.pddResponse", isbn)
+
+ if bookPicSMap == nil {
+ bookPicSMap = make(map[string]interface{})
+ }
+ bookPicSMap["pddResponse"] = bookPicNewPddPath
+ bookData["book_pic_s"] = bookPicSMap
+
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Printf("异步更新数据库和Redis过程发生panic: %v, ISBN: %s", r, isbn)
+ }
+ }()
+
+ bookPicSJSON, _ := json.Marshal(bookPicSMap)
+ updateQuery := "UPDATE book_center SET book_pic_s = ? WHERE isbn = ?"
+ if _, err := bc.db.Exec(updateQuery, string(bookPicSJSON), isbn); err != nil {
+ log.Printf("❌ 更新数据库 book_pic_s 失败: %v, ISBN: %s", err, isbn)
+ } else {
+ log.Printf("✅ 成功更新数据库 book_pic_s 字段, ISBN: %s", isbn)
+ }
+
+ if resultJSON, err := json.Marshal(bookData); err == nil {
+ if err := bc.redis.Set(context.Background(), isbn, resultJSON, 0).Err(); err != nil {
+ log.Printf("❌ 更新Redis缓存失败: %v, ISBN: %s", err, isbn)
+ } else {
+ log.Printf("✅ 成功更新Redis缓存, ISBN: %s", isbn)
+ }
+ }
+ }()
+ return
+ }
+
+ if !needsUpload {
+ log.Printf("📸 ISBN %s 的图片字段已完整,无需上传", isbn)
+ return
+ }
+
+ // ✅ 获取可上传的图片路径
+ var imageToUpload string
+
+ if path, ok := bookPicNewMap["pddPath"].(string); ok && path != "" {
+ imageToUpload = path
+ }
+ if imageToUpload == "" {
+ if localPath, ok := bookPicSMap["localPath"].(string); ok && localPath != "" {
+ imageToUpload = localPath
+ }
+ }
+ if imageToUpload == "" {
+ bookPic := getString(bookData, "book_pic")
+ if bookPic != "" {
+ if !strings.HasPrefix(bookPic, "http://") && !strings.HasPrefix(bookPic, "https://") {
+ bookPic = "https://" + bookPic
+ log.Printf("🔗 自动补全为完整URL: %s", bookPic)
+ }
+ imageToUpload = bookPic
+ }
+ }
+
+ log.Printf("📸 选中的上传图片路径: %s", imageToUpload)
+ if imageToUpload == "" {
+ log.Printf("⚠️ 没有找到可上传的图片路径,跳过上传")
+ return
+ }
+
+ uploadResp, err := UploadBookImage(imageToUpload)
+ if err != nil {
+ log.Printf("❌ 图片上传失败: %v", err)
+ return
+ }
+ log.Printf("✅ 图片上传成功,返回: %s", uploadResp)
+
+ if bookPicNewEmpty {
+ if bookPicNewMap == nil {
+ bookPicNewMap = make(map[string]interface{})
+ }
+ bookPicNewMap["pddPath"] = uploadResp
+ bookData["book_pic_new"] = bookPicNewMap
+ log.Printf("✅ 已更新 book_pic_new.pddPath: %s", uploadResp)
+ }
+
+ if bookPicSEmpty {
+ if bookPicSMap == nil {
+ bookPicSMap = make(map[string]interface{})
+ }
+ bookPicSMap["pddResponse"] = uploadResp
+ bookData["book_pic_s"] = bookPicSMap
+ log.Printf("✅ 已更新 book_pic_s.pddResponse: %s", uploadResp)
+ }
+
+ go func() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Printf("异步更新数据库和Redis过程发生panic: %v, ISBN: %s", r, isbn)
+ }
+ }()
+
+ if bookPicNewEmpty || bookPicSEmpty {
+ updateQuery := "UPDATE book_center SET "
+ updateParams := []interface{}{}
+ updateFields := []string{}
+
+ if bookPicNewEmpty {
+ bookPicNewJSON, _ := json.Marshal(bookPicNewMap)
+ updateFields = append(updateFields, "book_pic_new = ?")
+ updateParams = append(updateParams, string(bookPicNewJSON))
+ }
+ if bookPicSEmpty {
+ bookPicSJSON, _ := json.Marshal(bookPicSMap)
+ updateFields = append(updateFields, "book_pic_s = ?")
+ updateParams = append(updateParams, string(bookPicSJSON))
+ }
+ updateQuery += strings.Join(updateFields, ", ") + " WHERE isbn = ?"
+ updateParams = append(updateParams, isbn)
+
+ if _, err := bc.db.Exec(updateQuery, updateParams...); err != nil {
+ log.Printf("❌ 更新数据库失败: %v, ISBN: %s", err, isbn)
+ } else {
+ log.Printf("✅ 成功更新数据库图片字段, ISBN: %s", isbn)
+ }
+ }
+
+ if resultJSON, err := json.Marshal(bookData); err == nil {
+ if err := bc.redis.Set(context.Background(), isbn, resultJSON, 0).Err(); err != nil {
+ log.Printf("❌ 更新Redis缓存失败: %v, ISBN: %s", err, isbn)
+ } else {
+ log.Printf("✅ 成功更新Redis缓存, ISBN: %s", isbn)
+ }
+ }
+ }()
+}
+
+// 查询数据库
+func (bc *BookCenterController) queryBookByIsbn(isbn string) (map[string]interface{}, error) {
+ query := `SELECT isbn, book_name, author, publisher, fix_price, book_pic_s, book_pic_new
+ FROM book_center WHERE isbn = ?`
+
+ row := bc.db.QueryRow(query, isbn)
+ var isbnVal, bookName, author, publisher string
+ var fixPrice sql.NullInt64
+ var bookPicS, bookPicNew sql.NullString
+
+ err := row.Scan(&isbnVal, &bookName, &author, &publisher, &fixPrice, &bookPicS, &bookPicNew)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ return nil, err
+ }
+
+ result := map[string]interface{}{
+ "isbn": isbnVal,
+ "book_name": bookName,
+ "author": author,
+ "publisher": publisher,
+ "fix_price": fixPrice.Int64,
+ "book_pic_s": bookPicS.String,
+ "book_pic_new": bookPicNew.String,
+ }
+ return result, nil
+}
+
+// 工具:转换 Excel 单元格为 int
+func toInt(s string) int {
+ v, _ := strconv.Atoi(s)
+ return v
+}
+
+type BookPicS struct {
+ LocalPath string
+ PddResponse string
+}
+
+// 自定义类型 + UnmarshalJSON
+func (b *BookPicS) UnmarshalJSON(data []byte) error {
+ // 尝试解析为对象
+ var obj struct {
+ LocalPath string `json:"localPath"`
+ PddResponse string `json:"pddResponse"`
+ }
+ if err := json.Unmarshal(data, &obj); err == nil {
+ *b = BookPicS{
+ LocalPath: obj.LocalPath,
+ PddResponse: obj.PddResponse,
+ }
+ return nil
+ }
+
+ // 尝试解析为 string
+ var s string
+ if err := json.Unmarshal(data, &s); err == nil {
+ *b = BookPicS{
+ LocalPath: s,
+ PddResponse: "",
+ }
+ return nil
+ }
+
+ return fmt.Errorf("无法解析 BookPicS: %s", string(data))
+}
diff --git a/md/API.md b/md/API.md
new file mode 100644
index 0000000..507dce8
--- /dev/null
+++ b/md/API.md
@@ -0,0 +1,664 @@
+# 图书中心系统 API 文档
+
+## 基础信息
+
+- **Base URL**: `http://localhost:9009`
+- **API 版本**: v1.0
+- **数据格式**: JSON
+- **字符编码**: UTF-8
+
+---
+
+## 目录
+
+- [图书管理](#图书管理)
+- [搜索服务](#搜索服务)
+- [销量管理](#销量管理)
+- [图片管理](#图片管理)
+- [系统监控](#系统监控)
+- [Elasticsearch 操作](#elasticsearch-操作)
+
+---
+
+## 通用响应格式
+
+### 成功响应
+
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": { ... }
+}
+```
+
+### 错误响应
+
+```json
+{
+ "error": "错误描述",
+ "details": "详细错误信息"
+}
+```
+
+### 分页响应
+
+```json
+{
+ "current_page": 1,
+ "data": [...],
+ "per_page": 10,
+ "total": 100
+}
+```
+
+---
+
+## 图书管理
+
+### 1. 根据 ISBN 查询图书
+
+**请求**:
+```http
+GET /api/book/isbn?isbn=9787111111111
+```
+
+**参数**:
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| isbn | string | 是 | 图书ISBN号 |
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "id": 123456,
+ "isbn": "9787111111111",
+ "book_name": "Go语言编程",
+ "author": "作者名",
+ "publisher": "清华大学出版社",
+ "fix_price": 99.00,
+ "book_pic": "http://example.com/image.jpg",
+ "update_time": "1705228800"
+ }
+}
+```
+
+### 2. 添加图书到 ES
+
+**请求**:
+```http
+POST /api/es/book
+Content-Type: application/json
+
+{
+ "book_name": "Go语言编程实战",
+ "isbn": "9787111122222",
+ "author": "作者名",
+ "publisher": "出版社",
+ "fix_price": 89.00,
+ "book_pic": {
+ "localPath": "",
+ "pddPath": "http://example.com/image.jpg"
+ },
+ "book_pic_s": {
+ "localPath": "",
+ "pddResponse": "http://example.com/image_s.jpg"
+ }
+}
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "id": 123457,
+ "source": "service"
+ }
+}
+```
+
+### 3. 批量添加图书
+
+**请求**:
+```http
+POST /api/es/books/batch
+Content-Type: application/json
+
+{
+ "books": [
+ {
+ "book_name": "Go语言编程",
+ "isbn": "9787111111111",
+ "author": "作者1",
+ "publisher": "出版社1"
+ },
+ {
+ "book_name": "Python实战",
+ "isbn": "9787111133333",
+ "author": "作者2",
+ "publisher": "出版社2"
+ }
+ ]
+}
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "批量插入完成",
+ "data": {
+ "total_count": 2,
+ "success_count": 2,
+ "failed_count": 0,
+ "results": [
+ {
+ "index": 0,
+ "isbn": "9787111111111",
+ "success": true,
+ "document": "9787111111111"
+ },
+ {
+ "index": 1,
+ "isbn": "9787111133333",
+ "success": true,
+ "document": "9787111133333"
+ }
+ ]
+ }
+}
+```
+
+### 4. 检查图书是否存在
+
+**请求**:
+```http
+GET /api/es/book/exists?isbn=9787111111111
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "success": true,
+ "data": {
+ "exists": true,
+ "isbn": "9787111111111",
+ "book_id": "123456",
+ "book_name": "Go语言编程",
+ "message": "书籍存在"
+ }
+}
+```
+
+### 5. 更新图书字段
+
+**请求**:
+```http
+PUT /api/es/book/fields
+Content-Type: application/json
+
+{
+ "isbn": "9787111111111",
+ "data": {
+ "fix_price": 88.00,
+ "author": "新作者名",
+ "total_sale": 1000
+ }
+}
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "isbn": "9787111111111",
+ "updated": 1,
+ "fields_updated": 3,
+ "updated_fields": ["fix_price", "author", "total_sale"]
+ }
+}
+```
+
+### 6. 删除图书
+
+**请求**:
+```http
+DELETE /api/es/book?isbn=9787111111111
+```
+
+或
+
+```http
+DELETE /api/es/book?id=123456
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "deleted": true
+ }
+}
+```
+
+---
+
+## 搜索服务
+
+### 1. 关键词搜索
+
+**请求**:
+```http
+GET /api/search?keyword=Go语言&page=1&pageSize=10
+```
+
+**参数**:
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| keyword | string | 是 | 搜索关键词 |
+| page | int | 否 | 页码,默认1 |
+| pageSize | int | 否 | 每页数量,默认10 |
+
+**响应示例**:
+```json
+{
+ "count": 100,
+ "data": [
+ {
+ "id": 123456,
+ "book_name": "Go语言编程",
+ "author": "作者名",
+ "isbn": "9787111111111",
+ "publisher": "清华大学出版社",
+ "fix_price": 99.00
+ }
+ ]
+}
+```
+
+### 2. 多条件搜索
+
+**请求**:
+```http
+GET /api/search/conditions?book_name=Go&author=张三&page=1&pageSize=20
+```
+
+**支持的查询参数**:
+- `book_name` - 书名
+- `isbn` - ISBN
+- `author` - 作者
+- `category` - 分类
+- `publisher` - 出版社
+- `publication_time` - 出版时间范围(逗号分隔)
+- `day_sale_7` - 7天销量范围
+- `total_sale` - 总销量范围
+- `sell_counts` - 在售数量范围
+- `is_suit` - 是否套装 (0/1)
+- `is_return` - 是否驳回 (0/1)
+- `is_filter` - 过滤字段
+- `book_pic` - 是否有图 (0/1)
+- `picType` - 图片类型 (1-官图, 2-小图)
+- `saleSelect` - 销量维度选择 (7/15/30/60/90/180/365/0/1)
+- `categoryType` - 分类类型 (1-教材, 其他-非教材)
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "current_page": 1,
+ "data": [...],
+ "per_page": 20,
+ "total": 156
+}
+```
+
+### 3. 全字段搜索
+
+**请求**:
+```http
+GET /api/search/all?q=Go语言编程
+```
+
+**参数**:
+- `q` - 搜索关键词(搜索所有字段)
+
+---
+
+## 销量管理
+
+### 1. 更新在售数量
+
+**请求**:
+```http
+POST /api/sellcounts/update?isbn=9787111111111&onSaleCount=100
+```
+
+**参数**:
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| isbn | string | 是 | 图书ISBN |
+| onSaleCount | int | 是 | 在售数量(非负整数) |
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "isbn": "9787111111111",
+ "sell_counts": 100,
+ "async": true
+ }
+}
+```
+
+### 2. 从外部API更新在售数量
+
+**请求**:
+```http
+POST /api/sellcounts/fetch?isbn=9787111111111
+```
+
+**说明**: 自动调用 tail API 获取在售数量并更新
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "isbn": "9787111111111",
+ "on_sale_count": 150,
+ "async": true
+ }
+}
+```
+
+---
+
+## 图片管理
+
+### 1. 更新图书图片
+
+**请求**:
+```http
+POST /api/book/pic/update?isbn=9787111111111&book_pic=http://example.com/new.jpg&book_pic_s=http://example.com/new_s.jpg
+```
+
+**参数**:
+| 参数名 | 类型 | 必填 | 说明 |
+|--------|------|------|------|
+| isbn | string | 是 | 图书ISBN |
+| book_pic | string | 否 | 大图URL |
+| book_pic_s | string | 否 | 小图URL |
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "updated": 1
+ }
+}
+```
+
+---
+
+## 系统监控
+
+### 1. 健康检查
+
+**请求**:
+```http
+GET /health
+```
+
+**响应示例**:
+```json
+{
+ "status": "healthy",
+ "mysql": "connected",
+ "redis": "connected",
+ "elasticsearch": "connected",
+ "timestamp": "2025-01-14T10:30:00Z"
+}
+```
+
+### 2. 服务就绪检查
+
+**请求**:
+```http
+GET /ready
+```
+
+**响应示例**:
+```json
+{
+ "ready": true
+}
+```
+
+---
+
+## Elasticsearch 操作
+
+### 1. 查询所有索引
+
+**请求**:
+```http
+GET /api/es/indices
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": [
+ "books-from-mysql-v2",
+ "books-from-mysql-new",
+ "test-go-index"
+ ]
+}
+```
+
+### 2. 获取索引详情
+
+**请求**:
+```http
+GET /api/es/index/detail?indexName=books-from-mysql-v2
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "books-from-mysql-v2": {
+ "aliases": {},
+ "mappings": { ... },
+ "settings": { ... }
+ }
+ }
+}
+```
+
+### 3. 获取文档数量
+
+**请求**:
+```http
+GET /api/es/index/count?indexName=books-from-mysql-v2
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "count": 1000000
+ }
+}
+```
+
+### 4. 检查套装书
+
+**请求**:
+```http
+GET /api/book/check-suit?bookName=Go语言编程(套装)
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "book_name": "Go语言编程(套装)",
+ "is_suit": true
+ }
+}
+```
+
+### 5. 更新套装标记
+
+**请求**:
+```http
+PUT /api/book/suit
+Content-Type: application/json
+
+{
+ "isbn": "9787111111111"
+}
+```
+
+**说明**: 自动根据书名判断是否为套装书并更新
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": {
+ "isbn": "9787111111111",
+ "book_name": "Go语言编程(套装)",
+ "is_suit": 1,
+ "updated": 1,
+ "contains_suit_keyword": true
+ }
+}
+```
+
+### 6. 统计ID范围数量
+
+**请求**:
+```http
+GET /api/es/count/range?minID=1&maxID=100000
+```
+
+**响应示例**:
+```json
+{
+ "code": 200,
+ "minID": 1,
+ "maxID": 100000,
+ "count": 50000
+}
+```
+
+---
+
+## 错误码说明
+
+| 错误码 | 说明 |
+|--------|------|
+| 200 | 成功 |
+| 400 | 请求参数错误 |
+| 404 | 资源不存在 |
+| 500 | 服务器内部错误 |
+
+---
+
+## 请求示例
+
+### 使用 curl
+
+```bash
+# 查询图书
+curl "http://localhost:9009/api/book/isbn?isbn=9787111111111"
+
+# 搜索图书
+curl "http://localhost:9009/api/search?keyword=Go语言&page=1&pageSize=10"
+
+# 添加图书
+curl -X POST http://localhost:9009/api/es/book \
+ -H "Content-Type: application/json" \
+ -d '{"book_name":"Go语言编程","isbn":"9787111111111","author":"作者名"}'
+
+# 更新在售数量
+curl -X POST "http://localhost:9009/api/sellcounts/update?isbn=9787111111111&onSaleCount=100"
+```
+
+### 使用 Go
+
+```go
+import "net/http"
+
+// 查询图书
+resp, err := http.Get("http://localhost:9009/api/book/isbn?isbn=9787111111111")
+if err != nil {
+ log.Fatal(err)
+}
+defer resp.Body.Close()
+
+// 解析响应
+var result map[string]interface{}
+json.NewDecoder(resp.Body).Decode(&result)
+fmt.Println(result)
+```
+
+### 使用 JavaScript
+
+```javascript
+// 查询图书
+fetch('http://localhost:9009/api/book/isbn?isbn=9787111111111')
+ .then(response => response.json())
+ .then(data => console.log(data))
+ .catch(error => console.error(error));
+```
+
+---
+
+## 注意事项
+
+1. **所有时间戳格式**: Unix 时间戳(秒)
+2. **价格单位**: 分(需要除以100转换为元)
+3. **分页参数**: page 从 1 开始
+4. **异步操作**: 部分更新操作为异步执行,返回 `async: true`
+5. **CORS**: 默认允许跨域请求
+
+---
+
+## 更新日志
+
+### v1.0.0 (2025-01-14)
+- 初始 API 文档
+- 完整的图书管理接口
+- Elasticsearch 操作接口
+- 销量管理接口
+
+---
+
+**最后更新**: 2025-01-14
diff --git a/md/PROJECT_GUIDE.md b/md/PROJECT_GUIDE.md
new file mode 100644
index 0000000..811bf58
--- /dev/null
+++ b/md/PROJECT_GUIDE.md
@@ -0,0 +1,817 @@
+# 图书中心系统 - 开发指南
+
+## 📚 目录
+
+- [项目概述](#项目概述)
+- [技术栈](#技术栈)
+- [项目结构](#项目结构)
+- [环境配置](#环境配置)
+- [开发指南](#开发指南)
+- [API 文档](#api-文档)
+- [Elasticsearch 集成](#elasticsearch-集成)
+- [第三方服务集成](#第三方服务集成)
+- [数据库设计](#数据库设计)
+- [部署指南](#部署指南)
+- [常见问题](#常见问题)
+
+---
+
+## 项目概述
+
+图书中心管理系统是一个基于 Go 语言开发的综合性图书信息管理平台,集成 Elasticsearch 搜索引擎、Redis 缓存、MySQL 数据存储,以及多个第三方服务(孔夫子图书网、拼多多等)。
+
+### 核心功能
+
+- 📖 **图书管理** - 图书信息的增删改查、批量导入导出
+- 🔍 **智能搜索** - 基于 Elasticsearch 的全文检索
+- 📊 **销量统计** - 多维度销量数据统计与分析
+- 🖼️ **图片管理** - 图书封面图片上传与管理
+- 🔗 **第三方集成** - 孔夫子图书网、拼多多 API 集成
+- 📈 **数据监控** - SQL 性能监控、健康检查
+
+---
+
+## 技术栈
+
+### 后端框架
+- **Gin** - HTTP Web 框架
+- **GORM** - ORM 框架(部分使用)
+
+### 数据存储
+- **MySQL 8.0+** - 主数据库
+- **Redis 6.0+** - 缓存与会话存储
+- **Elasticsearch 8.x** - 搜索引擎
+
+### Go 库依赖
+```go
+require (
+ github.com/gin-gonic/gin v1.9.1
+ github.com/go-redis/redis/v8 v8.11.5
+ github.com/elastic/go-elasticsearch/v8 v8.11.1
+ github.com/go-sql-driver/mysql v1.7.1
+ github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/xuri/excelize/v2 v2.8.0
+ github.com/gorilla/websocket v1.5.1
+ github.com/gin-contrib/cors v1.4.0
+)
+```
+
+### 工具库
+- **go-cache** - 内存缓存
+- **excelize** - Excel 文件处理
+- **websocket** - WebSocket 通信支持
+
+---
+
+## 项目结构
+
+```
+centerBook/
+├── main.go # 主程序入口
+├── go.mod / go.sum # Go 模块依赖
+│
+├── es/ # Elasticsearch 模块
+│ ├── es_client.go # ES 客户端封装
+│ ├── es_search.go # ES 搜索服务
+│ ├── es_dll.go # DLL 接口封装(已弃用)
+│ ├── es_dll_test.go # DLL 单元测试
+│ └── DLL测试说明.md # DLL 测试文档
+│
+├── es_dll/ # ES DLL 库
+│ ├── es.dll # Windows DLL 文件
+│ └── es.md # DLL 接口文档
+│
+├── erp/ # ERP 模块
+│ └── erp_api.go # ERP API 接口
+│
+├── image/ # 图片处理模块
+│ └── image.go # 图片上传与管理
+│
+├── kongfz/ # 孔夫子图书网集成
+│ └── kongfz.go # 孔夫子 API 封装
+│
+├── tail/ # 第三方服务集成
+│ └── tail.go # Tail API(销量数据等)
+│
+├── middleware/ # 中间件
+│ └── middleware.go # 自定义中间件
+│
+├── health_api.go # 健康检查 API
+├── logging_middleware.go # 日志中间件
+├── pricing.go # 定价相关功能
+├── sql_monitor.go # SQL 性能监控
+├── ip_log.go # IP 日志记录
+│
+├── README.md # 项目说明
+└── PROJECT_GUIDE.md # 本文档
+```
+
+---
+
+## 环境配置
+
+### 开发环境要求
+
+- **Go** 1.19 或更高版本
+- **MySQL** 8.0+
+- **Redis** 6.0+
+- **Elasticsearch** 8.x (可选)
+
+### 配置步骤
+
+#### 1. 克隆项目
+
+```bash
+git clone
+cd centerBook
+```
+
+#### 2. 安装依赖
+
+```bash
+go mod tidy
+```
+
+#### 3. 数据库配置
+
+在 `main.go` 中修改数据库连接:
+
+```go
+// MySQL 主数据库
+targetDSN := "username:password@tcp(host:port)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+
+// 指数数据库
+zhishuDSN := "username:password@tcp(host:port)/zhishu?charset=utf8mb4&parseTime=True&loc=Local"
+```
+
+#### 4. Redis 配置
+
+```go
+rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "", // Redis 密码
+ DB: 0, // 使用默认 DB
+})
+```
+
+#### 5. Elasticsearch 配置
+
+```go
+esClient, err := elasticsearch.NewDefaultClient()
+// 或指定配置
+cfg := elasticsearch.Config{
+ Addresses: []string{
+ "http://localhost:9200",
+ },
+}
+esClient, err := elasticsearch.NewClient(cfg)
+```
+
+#### 6. 运行项目
+
+```bash
+# 开发模式
+go run main.go
+
+# 编译后运行
+go build -o centerBook.exe main.go
+./centerBook.exe
+```
+
+服务将在 `http://localhost:9009` 启动
+
+---
+
+## 开发指南
+
+### 代码规范
+
+#### 1. 命名约定
+
+- **文件名**:使用 `snake_case`,如 `es_search.go`
+- **包名**:使用小写单词,如 `es`, `image`, `kongfz`
+- **函数名**:导出函数使用 `PascalCase`,私有函数使用 `camelCase`
+- **常量**:全大写下划线分隔,如 `ESIndex`
+
+#### 2. 错误处理
+
+```go
+// 推荐:包装错误信息
+if err != nil {
+ return fmt.Errorf("查询图书失败: %w", err)
+}
+
+// 推荐:记录日志并返回
+if err != nil {
+ log.Printf("[Error] 查询失败: %v", err)
+ return nil, err
+}
+```
+
+#### 3. 日志规范
+
+```go
+// 日志格式:[模块名] 操作描述 | 关键信息
+log.Printf("[SearchBookByISBN] 开始查询 | ISBN=%s", isbn)
+log.Printf("[SearchBookByISBN] 查询成功 | 书名=%s", book.BookName)
+log.Printf("[SearchBookByISBN] 查询失败 | err=%v", err)
+```
+
+### 添加新功能
+
+#### 1. 创建新模块
+
+```bash
+# 在项目根目录创建模块目录
+mkdir newmodule
+
+# 创建模块文件
+touch newmodule/newmodule.go
+```
+
+#### 2. 模块模板
+
+```go
+package newmodule
+
+import (
+ "log"
+ "fmt"
+)
+
+// NewModuleService 创建新模块服务
+type NewModuleService struct {
+ // 依赖项
+}
+
+// NewService 初始化服务
+func NewService() *NewModuleService {
+ return &NewModuleService{}
+}
+
+// DoSomething 执行某项操作
+func (s *NewModuleService) DoSomething(input string) (string, error) {
+ log.Printf("[NewModule] 开始处理 | input=%s", input)
+
+ // 业务逻辑
+
+ return result, nil
+}
+```
+
+#### 3. 注册路由
+
+在 `main.go` 中添加路由:
+
+```go
+// 初始化服务
+newModuleSvc := newmodule.NewService()
+
+// 注册路由
+r.GET("/api/newmodule/action", func(c *gin.Context) {
+ result, err := newModuleSvc.DoSomething(c.Query("input"))
+ if err != nil {
+ c.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+ c.JSON(200, gin.H{"data": result})
+})
+```
+
+---
+
+## API 文档
+
+### 基础信息
+
+- **Base URL**: `http://localhost:9009`
+- **数据格式**: JSON
+- **字符编码**: UTF-8
+
+### 响应格式
+
+#### 成功响应
+```json
+{
+ "code": 200,
+ "message": "success",
+ "data": { ... }
+}
+```
+
+#### 错误响应
+```json
+{
+ "error": "错误描述",
+ "details": "详细错误信息"
+}
+```
+
+### 主要 API 端点
+
+#### 1. 健康检查
+
+```http
+GET /health
+```
+
+**响应**:
+```json
+{
+ "status": "healthy",
+ "timestamp": "2025-01-14T10:30:00Z"
+}
+```
+
+#### 2. 图书搜索
+
+```http
+GET /search/book?keyword=Go语言&page=1&pageSize=10
+```
+
+**参数**:
+- `keyword`: 搜索关键词
+- `page`: 页码(从1开始)
+- `pageSize`: 每页数量
+
+**响应**:
+```json
+{
+ "code": 200,
+ "data": {
+ "current_page": 1,
+ "data": [...],
+ "per_page": 10,
+ "total": 100
+ }
+}
+```
+
+#### 3. 根据 ISBN 查询图书
+
+```http
+GET /book/isbn?isbn=9787111111111
+```
+
+#### 4. 批量导入图书
+
+```http
+POST /books/batch
+Content-Type: application/json
+
+{
+ "books": [
+ {
+ "isbn": "9787111111111",
+ "book_name": "Go语言编程",
+ "author": "作者名",
+ ...
+ }
+ ]
+}
+```
+
+更多 API 详情请参考 `main.go` 中的路由定义。
+
+---
+
+## Elasticsearch 集成
+
+### 索引结构
+
+#### 索引名称
+```go
+const ESIndex = "books-from-mysql-v2"
+```
+
+#### 文档结构
+
+```go
+type ESBook struct {
+ ID int64 `json:"id"`
+ BookName FlexibleString `json:"book_name"`
+ BookPic BookPicObj `json:"book_pic"`
+ BookPicS BookPicSObj `json:"book_pic_s"`
+ ISBN string `json:"isbn"`
+ Author string `json:"author"`
+ Publisher string `json:"publisher"`
+ PublicationTime string `json:"publication_time"`
+ FixPrice float64 `json:"fix_price"`
+ // ... 更多字段
+}
+```
+
+### 使用示例
+
+#### 1. 搜索图书
+
+```go
+import "centerBook/es"
+
+// 初始化搜索服务
+searchSvc := es.NewESSearchService(esClient)
+
+// 关键词搜索
+books, err := searchSvc.SearchBooks("Go语言编程")
+
+// 多条件搜索
+books, total, err := searchSvc.SearchBooksByConditions(
+ map[string]string{
+ "author": "张三",
+ "publisher": "清华大学出版社",
+ },
+ 1, // page
+ 10, // pageSize
+)
+```
+
+#### 2. 添加图书到 ES
+
+```go
+req := &es.AddBookFullRequest{
+ BookName: "Go语言编程",
+ ISBN: "9787111111111",
+ Author: "作者名",
+ Publisher: "出版社",
+ // ... 其他字段
+}
+
+book, err := searchSvc.AddBookToES(ctx, req)
+```
+
+#### 3. 更新图书信息
+
+```go
+updateData := map[string]interface{}{
+ "fix_price": 99.00,
+ "author": "新作者名",
+}
+
+err := searchSvc.UpdateBookFieldsByISBNHandler(c)
+```
+
+### ES DLL 工具
+
+项目提供了 Windows DLL 工具用于直接操作 Elasticsearch(实验性功能)。
+
+#### 测试 DLL
+
+```bash
+# 查看所有索引
+go run test_es_dll.go list
+
+# 创建索引
+go run test_es_dll.go create test_index '{"mappings":{"properties":{"title":{"type":"text"}}}}'
+
+# 搜索文档
+go run test_es_dll.go search books-from-mysql-v2 '{"query":{"match_all":{}}}'
+```
+
+**注意**: DLL 功能处于实验阶段,`CreateDocument` 方法存在已知问题。
+
+---
+
+## 第三方服务集成
+
+### 孔夫子图书网 API
+
+**位置**: `kongfz/kongfz.go`
+
+```go
+import "centerBook/kongfz"
+
+// 根据 ISBN 获取图书图片和信息
+apiBook, err := kongfz.GetBookImageByISBN(
+ isbn,
+ "CALF_ELEPHANT_PROXY", // 代理类型
+ "1297757178467602432", // App Key
+ "QgQBvP7f", // App Secret
+)
+```
+
+### Tail API(销量数据)
+
+**位置**: `tail/tail.go`
+
+```go
+import "centerBook/tail"
+
+// 获取在售数量
+onSaleCount, err := tail.GetOnSaleCount(isbn)
+
+// 批量检查销量
+salesData, err := tail.CheckSales([]string{"isbn1", "isbn2"})
+```
+
+### 图片上传
+
+**位置**: `image/image.go`
+
+```go
+import "centerBook/image"
+
+// 下载并上传图书图片
+url, err := image.DownloadAndUploadBookImage(
+ imageURL,
+ isbn,
+ "true", // 是否使用代理
+ bookName,
+ "true", // 是否压缩
+)
+```
+
+---
+
+## 数据库设计
+
+### 主表:book_center
+
+**表结构**见 `README.md`,主要字段说明:
+
+| 字段名 | 类型 | 说明 |
+|--------|------|------|
+| id | BIGINT | 主键ID |
+| isbn | VARCHAR(30) | ISBN(唯一) |
+| isbn_hash | BIGINT | ISBN哈希(用于分区) |
+| book_name | VARCHAR(400) | 书名 |
+| author | VARCHAR(400) | 作者 |
+| publisher | VARCHAR(200) | 出版社 |
+| fix_price | INT | 定价(单位:分) |
+| day_sale_7 | INT | 7天销量 |
+| day_sale_30 | INT | 30天销量 |
+| total_sale | INT | 总销量 |
+| buy_counts | BIGINT | 已售数量 |
+| sell_counts | BIGINT | 在售数量 |
+
+### 索引设计
+
+```sql
+-- 主键
+PRIMARY KEY (`id`, `isbn_hash`, `isbn`)
+
+-- 唯一索引
+UNIQUE KEY `uk_isbn` (`isbn`, `isbn_hash`)
+
+-- 搜索索引
+KEY `idx_book_name` (`book_name` (100))
+KEY `idx_author` (`author` (50))
+KEY `idx_publisher` (`publisher` (50))
+KEY `idx_total_sale` (`total_sale`)
+KEY `idx_day_sale_7` (`day_sale_7`)
+KEY `idx_day_sale_30` (`day_sale_30`)
+
+-- 组合索引
+KEY `idx_core_search` (`day_sale_7`, `vio_book`, `book_set`, `onenum_mbooks`, `ill_publisher`, `ill_author`)
+```
+
+### 分区策略
+
+```sql
+PARTITION BY HASH (`isbn_hash`) PARTITIONS 10
+```
+
+---
+
+## 部署指南
+
+### 开发环境
+
+```bash
+# 1. 启动 MySQL
+# 2. 启动 Redis
+redis-server
+
+# 3. 启动 Elasticsearch(可选)
+# 4. 运行项目
+go run main.go
+```
+
+### 生产环境
+
+#### 使用 Docker
+
+```dockerfile
+FROM golang:1.19-alpine AS builder
+WORKDIR /app
+COPY .. .
+RUN go mod tidy && go build -o centerBook main.go
+
+FROM alpine:latest
+RUN apk --no-cache add ca-certificates
+WORKDIR /root/
+COPY --from=builder /app/centerBook .
+EXPOSE 9009
+CMD ["./centerBook"]
+```
+
+#### 构建和运行
+
+```bash
+# 构建镜像
+docker build -t centerbook:latest .
+
+# 运行容器
+docker run -d \
+ -p 9009:9009 \
+ -e DB_HOST=mysql \
+ -e DB_PASSWORD=password \
+ -e REDIS_HOST=redis \
+ --name centerbook \
+ centerbook:latest
+```
+
+### Windows 服务部署
+
+```bash
+# 使用 NSSM 将程序注册为 Windows 服务
+nssm install CenterBook "C:\path\to\centerBook.exe"
+nssm start CenterBook
+```
+
+---
+
+## 常见问题
+
+### 1. DLL 加载失败
+
+**问题**: `es DLL 不存在`
+
+**解决方案**:
+- 确保 `es_dll/es.dll` 文件存在
+- 检查文件路径是否正确
+- 确认 DLL 是为 Windows 平台编译的
+
+### 2. ES 连接失败
+
+**问题**: `无法连接到 Elasticsearch`
+
+**解决方案**:
+```go
+// 检查 ES 配置
+cfg := elasticsearch.Config{
+ Addresses: []string{"http://localhost:9200"},
+ Username: "elastic",
+ Password: "password",
+}
+```
+
+### 3. 数据库连接池耗尽
+
+**问题**: `too many connections`
+
+**解决方案**:
+```go
+// 调整连接池配置
+db.SetMaxOpenConns(150)
+db.SetMaxIdleConns(50)
+db.SetConnMaxLifetime(30 * time.Minute)
+```
+
+### 4. Redis 连接超时
+
+**问题**: `redis: connection timeout`
+
+**解决方案**:
+```go
+rdb := redis.NewClient(&redis.Options{
+ Addr: "localhost:6379",
+ Password: "",
+ DB: 0,
+ DialTimeout: 10 * time.Second,
+ ReadTimeout: 5 * time.Second,
+ WriteTimeout: 5 * time.Second,
+ PoolSize: 10,
+})
+```
+
+---
+
+## 性能优化
+
+### 1. 查询优化
+
+- 使用 ES 进行全文检索,避免 MySQL LIKE 查询
+- 合理使用 Redis 缓存热点数据
+- 对大表查询使用分页
+
+### 2. 并发优化
+
+```go
+// 使用 goroutine 并发处理
+var wg sync.WaitGroup
+results := make(chan Result, 10)
+
+for _, isbn := range isbns {
+ wg.Add(1)
+ go func(isbn string) {
+ defer wg.Done()
+ result := searchByISBN(isbn)
+ results <- result
+ }(isbn)
+}
+
+go func() {
+ wg.Wait()
+ close(results)
+}()
+```
+
+### 3. 缓存策略
+
+```go
+// 使用 go-cache 做内存缓存
+cache := cache.New(5*time.Minute, 10*time.Minute)
+
+// 设置缓存
+cache.Set("key", value, cache.DefaultExpiration)
+
+// 获取缓存
+if value, found := cache.Get("key"); found {
+ return value
+}
+```
+
+---
+
+## 测试
+
+### 运行单元测试
+
+```bash
+# 运行所有测试
+go test ./...
+
+# 运行 ES 模块测试
+cd es
+go test -v
+
+# 运行特定测试
+go test -v -run TestSearchBooks
+```
+
+### DLL 功能测试
+
+```bash
+# 运行完整测试
+go run test_es_dll.go test
+
+# 测试单个功能
+go run test_es_dll.go list
+go run test_es_dll.go search books-from-mysql-v2 '{"query":{"match_all":{}}}'
+```
+
+---
+
+## 贡献指南
+
+### 提交代码
+
+1. Fork 项目
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 打开 Pull Request
+
+### 代码审查清单
+
+- [ ] 代码符合项目规范
+- [ ] 添加了必要的注释
+- [ ] 更新了相关文档
+- [ ] 通过了所有测试
+- [ ] 没有引入新的警告
+
+---
+
+## 联系方式
+
+- **项目维护者**: 开发团队
+- **问题反馈**: 请提交 Issue
+- **文档更新**: 2025-01-14
+
+---
+
+## 附录
+
+### A. 相关文档
+
+- [README.md](README.md) - 项目说明
+- [es/DLL测试说明.md](es/DLL测试说明.md) - DLL 测试文档
+- [sql_monitor_usage.md](sql_monitor_usage.md) - SQL 监控使用说明
+
+### B. 外部资源
+
+- [Gin 框架文档](https://gin-gonic.com/docs/)
+- [Elasticsearch Go 客户端](https://github.com/elastic/go-elasticsearch)
+- [Go Redis 客户端](https://redis.uptrace.dev/)
+
+### C. 更新日志
+
+#### v1.0.0 (2025-01-14)
+- 初始版本发布
+- 完成 ES DLL 集成(实验性)
+- 添加完整的项目文档
+
+---
+
+**最后更新**: 2025-01-14
+**文档版本**: 1.0.0
diff --git a/md/README.md b/md/README.md
new file mode 100644
index 0000000..8fd50b3
--- /dev/null
+++ b/md/README.md
@@ -0,0 +1,336 @@
+# 图书中心管理系统 (Book Center Management System)
+
+## 项目简介
+
+图书中心管理系统是一个基于 Go 语言开发的图书信息管理平台,提供图书信息的增删改查、销量管理、图片上传等功能。系统采用 MySQL + Redis 双存储架构,支持高并发访问和数据缓存。
+
+## 技术栈
+
+- **后端框架**: Gin (Go Web Framework)
+- **数据库**: MySQL 8.0+
+- **缓存**: Redis 6.0+
+- **文件处理**: Excelize (Excel处理)
+- **WebSocket**: Gorilla WebSocket
+- **其他**: CORS支持、JWT认证、分页查询
+
+## 系统架构
+
+```
+┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
+│ 前端应用 │ │ Gin API服务 │ │ MySQL数据库 │
+│ │◄──►│ │◄──►│ │
+│ Web/Mobile │ │ 图书中心控制器 │ │ book_center │
+└─────────────────┘ └─────────────────┘ └─────────────────┘
+ │
+ ▼
+ ┌─────────────────┐
+ │ Redis缓存 │
+ │ │
+ │ 数据缓存/会话 │
+ └─────────────────┘
+```
+
+## 主要功能
+
+### 📚 图书管理
+- 图书信息查询(条件查询、随机查询)
+- 图书信息新增和更新
+- 图书封面图片管理
+- ISBN验证和格式检查
+
+### 📊 销量管理
+- 多维度销量统计(7天、15天、30天等)
+- 销量数据修改和更新
+- 年度销量统计
+
+### 🏷️ 违规管理
+- 批量设置违规标记
+- 根据ID或ISBN批量修改违规信息
+- 违规数据统计
+
+### 🖼️ 图片管理
+- 图书封面上传
+- 图片URL管理
+- 批量图片更新
+
+### 📈 数据导出
+- Excel格式数据导出
+- ISBN批量导出
+- 自定义查询结果导出
+
+## 快速开始
+
+### 环境要求
+
+- Go 1.19+
+- MySQL 8.0+
+- Redis 6.0+
+
+### 安装步骤
+
+1. **克隆项目**
+```bash
+git clone
+cd book-center-system
+```
+
+2. **安装依赖**
+```bash
+go mod tidy
+```
+
+3. **配置数据库**
+```sql
+-- 创建数据库
+CREATE DATABASE book_center CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+-- 导入数据表结构(见 database/schema.sql)
+```
+
+4. **配置Redis**
+```bash
+# 启动Redis服务
+redis-server
+```
+
+5. **修改配置**
+```go
+// 在 main.go 中修改数据库连接信息
+targetDSN := "username:password@tcp(host:port)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+
+// 修改Redis连接信息
+bookBaseRedisurl = "redis-host:port"
+```
+
+6. **运行项目**
+```bash
+go run main.go
+```
+
+服务将在 `http://localhost:9009` 启动
+
+## 配置说明
+
+### 数据库配置
+```go
+// 生产环境数据库
+targetDSN := "book_center:password@tcp(175.27.224.66:3306)/book_center?charset=utf8mb4&parseTime=True&loc=Local"
+
+// 连接池配置
+db.SetMaxOpenConns(150) // 最大打开连接数
+db.SetMaxIdleConns(50) // 最大空闲连接数
+db.SetConnMaxLifetime(30 * time.Minute) // 连接最大生存时间
+```
+
+### Redis配置
+```go
+rdb := redis.NewClient(&redis.Options{
+ Addr: "119.45.181.25:6666",
+ Password: "long6166",
+})
+```
+
+### CORS配置
+```go
+r.Use(cors.New(cors.Config{
+ AllowOrigins: []string{
+ "http://localhost:82",
+ "https://test.centerbook.buzhiyushu.cn"
+ },
+ AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
+ AllowCredentials: true,
+ MaxAge: 12 * time.Hour,
+}))
+```
+
+## 项目结构
+
+```
+book-center-system/
+├── main.go # 主程序入口
+├── README.md # 项目说明文档
+├── API.md # API接口文档
+├── USAGE.md # 使用说明文档
+├── go.mod # Go模块依赖
+├── go.sum # 依赖校验文件
+├── database/ # 数据库相关
+│ └── schema.sql # 数据表结构
+├── docs/ # 文档目录
+│ ├── images/ # 文档图片
+│ └── examples/ # 示例代码
+└── logs/ # 日志文件
+```
+
+## 部署说明
+
+### Docker部署
+
+```dockerfile
+FROM golang:1.19-alpine AS builder
+WORKDIR /app
+COPY .. .
+RUN go mod tidy && go build -o book-center main.go
+
+FROM alpine:latest
+RUN apk --no-cache add ca-certificates
+WORKDIR /root/
+COPY --from=builder /app/book-center .
+EXPOSE 9009
+CMD ["./book-center"]
+```
+
+### 系统服务部署
+```bash
+# 创建系统服务文件
+sudo vim /etc/systemd/system/book-center.service
+
+[Unit]
+Description=Book Center Management System
+After=network.target
+
+[Service]
+Type=simple
+User=www-data
+WorkingDirectory=/opt/book-center
+ExecStart=/opt/book-center/book-center
+Restart=always
+
+[Install]
+WantedBy=multi-user.target
+
+# 启动服务
+sudo systemctl enable book-center
+sudo systemctl start book-center
+```
+
+## 监控和日志
+
+### 健康检查
+- `GET /health` - 综合健康检查
+- `GET /ready` - 服务就绪检查
+
+### 日志级别
+系统使用标准的Go log包,支持以下日志级别:
+- INFO: 一般信息
+- WARN: 警告信息
+- ERROR: 错误信息
+
+### 性能监控
+- 数据库连接池监控
+- Redis连接状态监控
+- API响应时间统计
+- 内存使用情况
+
+## 贡献指南
+
+1. Fork 项目
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 打开 Pull Request
+
+## 许可证
+
+本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情
+
+## 联系方式
+
+- 项目维护者: [Your Name]
+- 邮箱: [your.email@example.com]
+- 项目地址: [GitHub Repository URL]
+
+## 更新日志
+
+### v1.0.0 (2025-08-06)
+- 初始版本发布
+- 基础图书管理功能
+- 销量统计功能
+- 违规管理功能
+- 图片上传功能
+- Excel导出功能
+
+表结构
+CREATE TABLE `book_center` (
+ `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'id',
+ `category` VARCHAR (40) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '分类',
+ `book_name` VARCHAR (400) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '书名',
+ `book_pic` VARCHAR (400) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '书图片',
+ `book_pic_s` JSON DEFAULT NULL COMMENT '官图json',
+ `isbn` VARCHAR (30) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT 'ISBN',
+ `isbn_hash` BIGINT UNSIGNED NOT NULL COMMENT 'ISBN哈希(用于分区)',
+ `author` VARCHAR (400) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '作者',
+ `editor` VARCHAR (30) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '编辑',
+ `binding_layout` VARCHAR (30) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '装帧',
+ `publisher` VARCHAR (200) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '出版社',
+ `edition` VARCHAR (50) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '版次',
+ `format` VARCHAR (50) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '开本',
+ `languages` VARCHAR (50) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '语种',
+ `publication_time` BIGINT DEFAULT NULL COMMENT '出版时间(时间戳)',
+ `print_time` BIGINT DEFAULT NULL COMMENT '印刷时间(时间戳)',
+ `paper` VARCHAR (500) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '纸张',
+ `pages` INT UNSIGNED DEFAULT NULL COMMENT '页数',
+ `wordage` VARCHAR (500) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '字数',
+ `fix_price` INT UNSIGNED DEFAULT NULL COMMENT '定价(单位:分)',
+ `content` VARCHAR (500) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '内容',
+ `remark` VARCHAR (500) CHARACTER
+ SET utf8mb4 COLLATE utf8mb4_bin DEFAULT '' COMMENT '备注',
+ `vio_book` TINYINT (1) NOT NULL DEFAULT '0' COMMENT '违规书号 (0-正常 1-违规)',
+ `book_set` TINYINT (1) NOT NULL DEFAULT '0' COMMENT '套装书 (0-否 1-是)',
+ `onenum_mbooks` TINYINT UNSIGNED DEFAULT NULL COMMENT '一号多书 (0-正常 1-违规)',
+ `ill_publisher` TINYINT (1) NOT NULL DEFAULT '0' COMMENT '违规出版社 (0-正常 1-违规)',
+ `ill_author` TINYINT (1) NOT NULL DEFAULT '0' COMMENT '违规作者(0-正常 1-违规)',
+ `day_sale_7` INT NOT NULL DEFAULT '0' COMMENT '7天内销量',
+ `day_sale_15` INT NOT NULL DEFAULT '0' COMMENT '15天内销量',
+ `day_sale_30` INT NOT NULL DEFAULT '0' COMMENT '30天内销量',
+ `day_sale_60` INT NOT NULL DEFAULT '0' COMMENT '60天内销量',
+ `day_sale_90` INT NOT NULL DEFAULT '0' COMMENT '90天内销量',
+ `day_sale_180` INT NOT NULL DEFAULT '0' COMMENT '180天内销量',
+ `day_sale_365` INT NOT NULL DEFAULT '0' COMMENT '365天内销量',
+ `this_year_sale` INT NOT NULL DEFAULT '0' COMMENT '今年销量',
+ `last_year_sale` INT NOT NULL DEFAULT '0' COMMENT '去年销量',
+ `total_sale` INT NOT NULL DEFAULT '0' COMMENT '总销量',
+ `sold_out_times` JSON DEFAULT NULL COMMENT '已售时间记录(JSON数组格式)',
+ `shipment_cycle` INT DEFAULT NULL COMMENT '出货周期(天)',
+ `publiction_times` BIGINT DEFAULT NULL COMMENT '出版时间(时间戳)',
+ `cat_id` BIGINT DEFAULT NULL COMMENT '叶子类目id',
+ `buy_counts` BIGINT UNSIGNED DEFAULT NULL COMMENT '已售数量',
+ `sell_counts` BIGINT UNSIGNED DEFAULT NULL COMMENT '在售数量',
+ `del_flag` TINYINT (1) NOT NULL DEFAULT '0' COMMENT '删除标志(0-存在 1-删除)',
+ `create_dept` BIGINT DEFAULT NULL COMMENT '创建部门',
+ `create_by` BIGINT DEFAULT NULL COMMENT '创建者',
+ `create_time` BIGINT NOT NULL COMMENT '创建时间(时间戳)',
+ `update_by` BIGINT DEFAULT NULL COMMENT '更新者',
+ `update_time` BIGINT (20) UNSIGNED ZEROFILL NOT NULL COMMENT '更新时间(时间戳)',
+ `libri_scolastici` TINYINT (1) DEFAULT NULL COMMENT '大学教材',
+ `libriScolastici` TINYINT (1) DEFAULT NULL COMMENT '大学教材',
+ `book_pic_new` JSON DEFAULT NULL COMMENT '书图片',
+ PRIMARY KEY (`id`, `isbn_hash`, `isbn`),
+ UNIQUE KEY `uk_isbn` (`isbn`, `isbn_hash`),
+ KEY `idx_book_name` (`book_name` (100)) USING BTREE,
+ KEY `idx_publisher` (`publisher` (50)) USING BTREE,
+ KEY `idx_author` (`author` (50)) USING BTREE,
+ KEY `idx_total_sale` (`total_sale`) USING BTREE,
+ KEY `idx_day_sale_7` (`day_sale_7`) USING BTREE,
+ KEY `idx_day_sale_30` (`day_sale_30`) USING BTREE,
+ KEY `idx_day_sale_365` (`day_sale_365`) USING BTREE,
+ KEY `idx_this_year_sale` (`this_year_sale`) USING BTREE,
+ KEY `idx_publication_times` (`publiction_times`) USING BTREE,
+ KEY `idx_book_pic` (`book_pic`) USING BTREE,
+ KEY `idx_id` (`id`) USING BTREE,
+ KEY `idx_core_search` (`day_sale_7`, `vio_book`, `book_set`, `onenum_mbooks`, `ill_publisher`, `ill_author`) USING BTREE,
+ KEY `idx_category` (`category`) USING BTREE,
+KEY `idx_counts` (`buy_counts`, `sell_counts`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 3780989 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '图书基础信息表(哈希分区 by ISBN hash)' /*!50100 PARTITION BY HASH (`isbn_hash`) PARTITIONS 10 */;
\ No newline at end of file
diff --git a/md/sql_monitor_usage.md b/md/sql_monitor_usage.md
new file mode 100644
index 0000000..1361e52
--- /dev/null
+++ b/md/sql_monitor_usage.md
@@ -0,0 +1,252 @@
+# SQL健康监控使用指南
+
+## 概述
+
+本项目已集成了简单而高效的SQL健康监控功能,可以自动记录每条SQL语句的执行时间、成功状态和错误信息。
+
+## 功能特性
+
+- ✅ 自动记录SQL执行时间(毫秒级精度)
+- ✅ 记录SQL执行成功/失败状态
+- ✅ 记录错误信息和影响行数
+- ✅ 支持慢查询检测
+- ✅ 提供统计信息(成功率、平均耗时等)
+- ✅ 提供Web仪表板实时监控
+- ✅ 内存高效(可配置最大记录数)
+
+## API接口
+
+### 1. 获取SQL执行统计信息
+```
+GET /api/sql-health/stats
+```
+
+响应示例:
+```json
+{
+ "status": "success",
+ "data": {
+ "total_queries": 1250,
+ "success_queries": 1240,
+ "failed_queries": 10,
+ "success_rate": "99.20",
+ "avg_duration_ms": 45,
+ "max_duration_ms": 2300,
+ "min_duration_ms": 2,
+ "last_update": "2024-01-15 14:30:25"
+ },
+ "timestamp": "2024-01-15 14:30:25"
+}
+```
+
+### 2. 获取最近的SQL执行记录
+```
+GET /api/sql-health/recent?limit=50
+```
+
+参数:
+- `limit`: 返回记录数量,默认50,最大500
+
+响应示例:
+```json
+{
+ "status": "success",
+ "data": {
+ "records": [
+ {
+ "id": 1001,
+ "query": "SELECT id, book_name FROM book_center WHERE vio_book = ? ORDER BY id DESC LIMIT ? OFFSET ?",
+ "duration_ms": 23,
+ "timestamp": "2024-01-15T14:30:25Z",
+ "success": true,
+ "error": "",
+ "endpoint": "/api/bookBase/GetBookBaseInfoOptimized",
+ "rows_affected": 0
+ }
+ ],
+ "count": 50,
+ "limit": 50
+ },
+ "timestamp": "2024-01-15 14:30:25"
+}
+```
+
+### 3. 获取慢查询
+```
+GET /api/sql-health/slow-queries?threshold=1000
+```
+
+参数:
+- `threshold`: 慢查询阈值(毫秒),默认1000ms
+
+### 4. 获取失败查询
+```
+GET /api/sql-health/failed-queries
+```
+
+### 5. 清空记录
+```
+POST /api/sql-health/clear
+```
+
+### 6. Web仪表板
+```
+GET /api/sql-health/dashboard
+```
+
+访问此URL可以查看实时的SQL健康监控仪表板。
+
+## 在代码中使用SQL监控
+
+### 1. 查询操作(返回多行)
+```go
+func (bc *BookCenterController) YourQueryMethod(c *gin.Context) {
+ query := "SELECT * FROM book_center WHERE category = ?"
+ endpoint := c.FullPath() // 获取当前接口路径
+
+ // 使用监控执行查询
+ rows, err := MonitorQuery(bc.db, query, endpoint, "小说")
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "查询失败"})
+ return
+ }
+ defer rows.Close()
+
+ // 处理结果...
+}
+```
+
+### 2. 单行查询操作
+```go
+func (bc *BookCenterController) GetBookByISBN(c *gin.Context) {
+ isbn := c.Query("isbn")
+ query := "SELECT id, book_name, author FROM book_center WHERE isbn = ?"
+ endpoint := c.FullPath()
+
+ // 使用监控执行单行查询
+ row := MonitorQueryRow(bc.db, query, endpoint, isbn)
+
+ var id int64
+ var bookName, author string
+ if err := row.Scan(&id, &bookName, &author); err != nil {
+ c.JSON(http.StatusNotFound, gin.H{"error": "图书不存在"})
+ return
+ }
+
+ // 返回结果...
+}
+```
+
+### 3. 更新/插入/删除操作
+```go
+func (bc *BookCenterController) UpdateBook(c *gin.Context) {
+ query := "UPDATE book_center SET book_name = ? WHERE id = ?"
+ endpoint := c.FullPath()
+
+ // 使用监控执行更新操作
+ result, err := MonitorExec(bc.db, query, endpoint, "新书名", 123)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败"})
+ return
+ }
+
+ rowsAffected, _ := result.RowsAffected()
+ c.JSON(http.StatusOK, gin.H{
+ "message": "更新成功",
+ "rows_affected": rowsAffected,
+ })
+}
+```
+
+## 监控配置
+
+### 初始化配置
+在 `main.go` 中的初始化:
+```go
+func main() {
+ // 初始化SQL监控器,最多保存1000条记录
+ InitSQLMonitor(1000)
+
+ // ... 其他初始化代码
+}
+```
+
+### 自定义配置
+可以根据需要调整最大记录数:
+```go
+// 保存更多记录(适合高流量应用)
+InitSQLMonitor(5000)
+
+// 保存较少记录(适合内存受限环境)
+InitSQLMonitor(500)
+```
+
+## 性能影响
+
+- **内存占用**: 每条记录约占用200-500字节
+- **CPU开销**: 每次SQL执行增加约0.1-0.5ms开销
+- **并发安全**: 使用读写锁,支持高并发访问
+- **自动清理**: 超过最大记录数时自动删除旧记录
+
+## 监控指标说明
+
+### 统计指标
+- `total_queries`: 总查询数
+- `success_queries`: 成功查询数
+- `failed_queries`: 失败查询数
+- `success_rate`: 成功率(百分比)
+- `avg_duration_ms`: 平均执行时间(毫秒)
+- `max_duration_ms`: 最大执行时间(毫秒)
+- `min_duration_ms`: 最小执行时间(毫秒)
+
+### 记录字段
+- `id`: 记录唯一ID
+- `query`: SQL查询语句
+- `duration_ms`: 执行时间(毫秒)
+- `timestamp`: 执行时间戳
+- `success`: 是否成功
+- `error`: 错误信息(如果有)
+- `endpoint`: 调用的API接口
+- `rows_affected`: 影响的行数(仅适用于INSERT/UPDATE/DELETE)
+
+## 最佳实践
+
+1. **接口标识**: 使用 `c.FullPath()` 作为endpoint参数,便于追踪问题
+2. **错误处理**: 监控函数会自动记录错误,无需额外处理
+3. **性能优化**: 对于非关键查询,可以考虑不使用监控以减少开销
+4. **定期清理**: 在生产环境中定期调用清理接口,避免内存占用过多
+5. **阈值设置**: 根据业务需求调整慢查询阈值
+
+## 故障排查
+
+### 常见问题
+1. **监控器未初始化**: 确保在main函数中调用了 `InitSQLMonitor()`
+2. **记录不显示**: 检查是否使用了 `MonitorQuery` 等监控函数
+3. **内存占用过高**: 减少最大记录数或定期清理记录
+
+### 调试技巧
+- 查看控制台日志,监控函数会输出详细的执行信息
+- 使用仪表板实时查看SQL执行情况
+- 通过API接口获取详细的统计信息
+
+## 示例场景
+
+### 场景1: 性能优化
+通过监控发现某个查询平均耗时过长,可以:
+1. 查看慢查询接口找到具体SQL
+2. 分析SQL执行计划
+3. 添加合适的索引
+4. 重新监控验证优化效果
+
+### 场景2: 错误排查
+当出现数据库错误时,可以:
+1. 查看失败查询接口
+2. 分析错误信息和SQL语句
+3. 定位问题原因
+4. 修复后验证
+
+### 场景3: 容量规划
+通过长期监控数据:
+1. 分析查询频率和耗时趋势
+2. 评估数据库负载
+3. 制定扩容计划
diff --git a/middleware/middleware.go b/middleware/middleware.go
new file mode 100644
index 0000000..7de946b
--- /dev/null
+++ b/middleware/middleware.go
@@ -0,0 +1,79 @@
+package middleware
+
+import (
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+type RateLimiter struct {
+ visitors map[string]*Visitor
+ mu sync.Mutex
+}
+
+type Visitor struct {
+ lastSeen time.Time
+ tokens int
+}
+
+func NewRateLimiter() *RateLimiter {
+ rl := &RateLimiter{
+ visitors: make(map[string]*Visitor),
+ }
+ // 定期清理过期访问者
+ go rl.cleanupVisitors()
+ return rl
+}
+
+func (rl *RateLimiter) cleanupVisitors() {
+ for {
+ time.Sleep(time.Minute)
+ rl.mu.Lock()
+ for ip, v := range rl.visitors {
+ if time.Since(v.lastSeen) > time.Minute {
+ delete(rl.visitors, ip)
+ }
+ }
+ rl.mu.Unlock()
+ }
+}
+
+func (rl *RateLimiter) LimitMiddleware(maxRequests int, per time.Duration) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ ip := c.ClientIP()
+ rl.mu.Lock()
+ defer rl.mu.Unlock()
+
+ v, exists := rl.visitors[ip]
+ if !exists {
+ rl.visitors[ip] = &Visitor{
+ lastSeen: time.Now(),
+ tokens: maxRequests - 1,
+ }
+ c.Next()
+ return
+ }
+
+ // 重置 token
+ if time.Since(v.lastSeen) > per {
+ v.tokens = maxRequests - 1
+ v.lastSeen = time.Now()
+ c.Next()
+ return
+ }
+
+ if v.tokens > 0 {
+ v.tokens--
+ v.lastSeen = time.Now()
+ c.Next()
+ return
+ }
+
+ // 超过限流
+ c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
+ "error": "请求过于频繁,请稍后再试",
+ })
+ }
+}
diff --git a/pricing.go b/pricing.go
new file mode 100644
index 0000000..b06b0a8
--- /dev/null
+++ b/pricing.go
@@ -0,0 +1,329 @@
+// go/main.go
+package main
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// 与 Java 保持一致的 RSA 公钥/私钥(Base64)
+var publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqmdgZjjpySFAd+3Go33tshTOhRcSl6Sl4x8bR5vrEzsvFqQQW+VXLco0E1jy9dIR4NguRIGWOowi/4EU5PEM3ZVrXQCxCnyyqIuuDtY9QNTh5DTn60aDOLEL2X7+mvICgFg+VAKPik+8fBSUfzcGiLqFlx+VhUAkq9hCyd/wtYInuAxPSoCr8F2cmI4/V6sAhVUkHUZhJvWlyDLUpYKOGgYM4rXjCXXKrPO0FNf1iY70AWACSJmXUwBVuIRYWfTRVOvzEPWkp/tuqir/XcvMfKKVU5/eCr8abNVIG99HTF1iKvQPdQUldAyk5z9YPV5IwAbrjlEACmJ5JvuT3bypewIDAQAB"
+var privateKeyStr = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqZ2BmOOnJIUB37cajfe2yFM6FFxKXpKXjHxtHm+sTOy8WpBBb5VctyjQTWPL10hHg2C5EgZY6jCL/gRTk8QzdlWtdALEKfLKoi64O1j1A1OHkNOfrRoM4sQvZfv6a8gKAWD5UAo+KT7x8FJR/NwaIuoWXH5WFQCSr2ELJ3/C1gie4DE9KgKvwXZyYjj9XqwCFVSQdRmEm9aXIMtSlgo4aBgziteMJdcqs87QU1/WJjvQBYAJImZdTAFW4hFhZ9NFU6/MQ9aSn+26qKv9dy8x8opVTn94Kvxps1Ugb30dMXWIq9A91BSV0DKTnP1g9XkjABuuOUQAKYnkm+5PdvKl7AgMBAAECggEAY0xmYmsb4PadiMVokXEaiEGTrv6o+PEbMeS4ktwK+mPsprboSYS1bpt8CSI2QoUtoeaX35fcITX0VwuzT04gfydJLyLuB/xuZ8Utoru5agQjtkYWN4YZhXm2PAHDACuyxXOmrnHnj2OzpGKhvhgkmJyIqG3hRYsBU5psIRN8Q2gnCarhiB948YDu6EfvFJPv0ET9T+mzQWLmMVz+lorepfcXV1Wwvr0awRPfyS2s7te9AW4GYEzKN9ijZx05XnYOSgh/hD82iqh+poPXiqamhZGcQBQ8CveVbyNatTpbZYca8gXByOIipSEqg3UVQ2rnd/hYAh4VKSagyKXtFt7REQKBgQDgjGtbit3JK8jJuQTWBqgNHWphc1/4tAidjWeEF1NBi0NZuSZiVLoh9J794lO67psAZheZV70gXD9pHF7tiSESTIsHZiOtU9rrEVACc6EHJIeBrmB4oNWvzzix5S0S6RZxLYm5oiEoNjXSfqcFqCHGiT3lnxUhSp3JywlcIx0QZQKBgQDCRYEMt+u0Hp8JbhxhHx5Z87dZRJe0AGRQXnz3blxq+MTIVB6oj6NKBxPN6BmNLGI/xnxO8XTog5JCv9SZRZpY6eZmdCt2sVK2k2i1kxpd3sLjlbFTNnyC9RJFeZMjIlvW03KmDu0VBDf4CW/zSP3aej+vgxvOZesYcgI9isoEXwKBgEmrJ+mflIXUfIpZzhFdm7K5zNXt4TWZ8x2lb6mxcVoWk2ETUll+TJapR6Qppai1cVrfI6zmUSEVwqP8b9RkYdo8DHy/8MKDuVXXlzVGtDTAskhEalgJBDIqvQH4GyKSIA+/jei+HTyxFFVbwfYkI/ibvBfiai9C6KN0njyBNJ7VAoGAVgDZCZ1efmXT+CPD8ocJM78+GwnPswM9ZYr+/bbguQaabyk2TV8RZdNORCiNLz9H233uSDCClfCxTlWIM7ZphxU9R3wERc5olKUbhM6zrHzSgFgjoXgMlRkTVqhkp/gs+iSvq64N7PDqKidbZTOaFh9qlDORmsTp1++Y6E/J8TcCgYEA0cszTR9OuvrQDpiX5PW+HB66GIxuEFBCla0HphtV16i/tzXnIaZ6q2hf1e9qejO3lOIzi3e1PVHOuMIemzl17batonERhBIjDYEtGraFyaHSgkp+zRdjPGj8A0dq7iwdv0M4ravQcF9dVvfEucVhN3XSJXSqdJRSoZzOvRZH4VY="
+
+// parsePublicKey 解析 Java Base64 公钥为 rsa.PublicKey
+// 返回 rsa.PublicKey 或错误
+func parsePublicKey(b64 string) (*rsa.PublicKey, error) {
+ der, err := base64.StdEncoding.DecodeString(b64)
+ if err != nil {
+ return nil, err
+ }
+ pub, err := x509ParsePublicKey(der)
+ return pub, err
+}
+
+// parsePrivateKey 解析 Java Base64 私钥为 rsa.PrivateKey
+// 返回 rsa.PrivateKey 或错误
+func parsePrivateKey(b64 string) (*rsa.PrivateKey, error) {
+ der, err := base64.StdEncoding.DecodeString(b64)
+ if err != nil {
+ return nil, err
+ }
+ priv, err := x509ParsePrivateKey(der)
+ return priv, err
+}
+
+// x509ParsePublicKey X509 公钥解析(PKCS#8)
+func x509ParsePublicKey(der []byte) (*rsa.PublicKey, error) {
+ key, err := x509.ParsePKIXPublicKey(der)
+ if err != nil {
+ return nil, err
+ }
+ pub, ok := key.(*rsa.PublicKey)
+ if !ok {
+ return nil, errors.New("公钥类型错误")
+ }
+ return pub, nil
+}
+
+// x509ParsePrivateKey PKCS#8 私钥解析
+func x509ParsePrivateKey(der []byte) (*rsa.PrivateKey, error) {
+ key, err := x509.ParsePKCS8PrivateKey(der)
+ if err != nil {
+ return nil, err
+ }
+ priv, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ return nil, errors.New("私钥类型错误")
+ }
+ return priv, nil
+}
+
+// EncryptHybrid 混合加密:随机 AES-256-GCM + RSA-OAEP(SHA-256) 加密 AES 密钥
+// 输出 Base64(iv || rsa(aesKey) || gcmCiphertext) 与 Java 完全一致
+func EncryptHybrid(plaintext []byte) (string, error) {
+ pub, err := parsePublicKey(publicKeyStr)
+ if err != nil {
+ return "", err
+ }
+ aesKey := make([]byte, 32)
+ if _, err = rand.Read(aesKey); err != nil {
+ return "", err
+ }
+ block, err := aes.NewCipher(aesKey)
+ if err != nil {
+ return "", err
+ }
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ return "", err
+ }
+ iv := make([]byte, 12)
+ if _, err = rand.Read(iv); err != nil {
+ return "", err
+ }
+ ciphertext := gcm.Seal(nil, iv, plaintext, nil)
+ label := []byte{}
+ encKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, pub, aesKey, label)
+ if err != nil {
+ return "", err
+ }
+ buf := bytes.Join([][]byte{iv, encKey, ciphertext}, nil)
+ return base64.StdEncoding.EncodeToString(buf), nil
+}
+
+// DecryptHybrid 混合解密:拆分 iv、RSA 加密的 AES 密钥、GCM 密文
+// 使用 RSA-OAEP(SHA-256) 解密 AES 密钥,再用 AES-256-GCM 解密数据
+func DecryptHybrid(token string) ([]byte, error) {
+ log.Println("=== 开始解密 DecryptHybrid ===")
+ log.Println("原始 token:", token)
+
+ // 兼容 Java 的 " "→"+"
+ token = strings.ReplaceAll(token, " ", "+")
+ log.Println("空格替换后的 token:", token)
+
+ data, err := base64.StdEncoding.DecodeString(token)
+ if err != nil {
+ log.Println("Base64 解码失败:", err)
+ return nil, err
+ }
+ log.Println("Base64 解码成功,长度:", len(data))
+
+ if len(data) < 12+256 {
+ log.Println("密文长度非法:", len(data))
+ return nil, errors.New("密文长度非法")
+ }
+
+ iv := data[:12]
+ encKey := data[12 : 12+256]
+ ct := data[12+256:]
+
+ log.Println("IV 长度:", len(iv))
+ log.Println("RSA 加密的 AES Key 长度:", len(encKey))
+ log.Println("GCM 密文长度:", len(ct))
+
+ priv, err := parsePrivateKey(privateKeyStr)
+ if err != nil {
+ log.Println("解析私钥失败:", err)
+ return nil, err
+ }
+
+ // RSA PKCS1Padding 解密
+ aesKey, err := rsa.DecryptPKCS1v15(rand.Reader, priv, encKey)
+ if err != nil {
+ log.Println("RSA PKCS1 解密失败:", err)
+ return nil, err
+ }
+ log.Println("RSA 解密 AES Key 成功,长度:", len(aesKey))
+
+ block, err := aes.NewCipher(aesKey)
+ if err != nil {
+ log.Println("AES Cipher 创建失败:", err)
+ return nil, err
+ }
+
+ gcm, err := cipher.NewGCM(block)
+ if err != nil {
+ log.Println("GCM 创建失败:", err)
+ return nil, err
+ }
+
+ pt, err := gcm.Open(nil, iv, ct, nil)
+ if err != nil {
+ log.Println("GCM 解密失败:", err)
+ return nil, err
+ }
+
+ log.Println("解密成功,明文长度:", len(pt))
+ log.Println("=== 解密结束 ===")
+ return pt, nil
+}
+
+// PricingLinkHandler 处理 GET /pricingLink
+// 构造与 Java 相同结构的载荷:number(=numbers*100)、total、qureyApiUrl、taskMapList,并进行混合加密
+func PricingLinkHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "仅支持 GET", http.StatusMethodNotAllowed)
+ return
+ }
+ q := r.URL.Query()
+ numbers := atoi(q.Get("numbers"))
+ total := atoi(q.Get("total"))
+
+ res := map[string]any{
+ "number": strconv.Itoa(numbers * 100),
+ "total": strconv.Itoa(total),
+ "qureyApiUrl": buildQueryApiUrl(q),
+ "taskMapList": buildTaskMapList(q),
+ }
+
+ body, err := json.Marshal(res)
+ if err != nil {
+ http.Error(w, "序列化失败", http.StatusInternalServerError)
+ return
+ }
+ token, err := EncryptHybrid(body)
+ if err != nil {
+ http.Error(w, "加密失败", http.StatusInternalServerError)
+ return
+ }
+ writeJSON(w, http.StatusOK, token)
+}
+
+// PricingLinkDecHandler 处理 GET /pricingLinkDec
+// 解密并返回原始 Map,兼容 Java 的 Base64 空格替换逻辑
+func PricingLinkDecHandler(w http.ResponseWriter, r *http.Request) {
+ log.Println("进入 /pricingLinkDec")
+
+ if r.Method != http.MethodGet {
+ log.Println("方法非法:", r.Method)
+ http.Error(w, "仅支持 GET", http.StatusMethodNotAllowed)
+ return
+ }
+
+ link := r.URL.Query().Get("link")
+ log.Println("收到 link 参数:", link)
+
+ if strings.TrimSpace(link) == "" {
+ log.Println("缺少 link 参数")
+ http.Error(w, "缺少参数 link", http.StatusBadRequest)
+ return
+ }
+
+ pt, err := DecryptHybrid(link)
+ if err != nil {
+ log.Println("解密失败:", err)
+ http.Error(w, "解密失败", http.StatusBadRequest)
+ return
+ }
+
+ var payload map[string]any
+ if err := json.Unmarshal(pt, &payload); err != nil {
+ log.Println("JSON 解析失败:", err)
+ http.Error(w, "载荷解析失败", http.StatusBadRequest)
+ return
+ }
+
+ log.Println("解密成功,返回 payload:", payload)
+ writeJSON(w, http.StatusOK, payload)
+}
+
+// buildQueryApiUrl 生成 qureyApiUrl,与 Java 的 getUrl 逻辑一致(按字段编码并拼接)
+// 基础路径固定为 https://api.buzhiyushu.cn/zhishu/baseInfo/pricing/list
+func buildQueryApiUrl(q url.Values) string {
+ base := "https://api.buzhiyushu.cn/zhishu/baseInfo/pricing/list?"
+ add := func(k string) string {
+ v := strings.TrimSpace(q.Get(k))
+ if v == "" {
+ return ""
+ }
+ return fmt.Sprintf("%s=%s&", k, url.QueryEscape(v))
+ }
+ var b strings.Builder
+ b.WriteString(base)
+ // 字段与 Java 对齐
+ b.WriteString(add("bookName"))
+ b.WriteString(add("bookPic"))
+ b.WriteString(add("isbn"))
+ b.WriteString(add("author"))
+ b.WriteString(add("publisher"))
+ if v := q.Get("vio_book"); v != "" {
+ b.WriteString(fmt.Sprintf("vio_book=%s&", url.QueryEscape(v)))
+ }
+ if v := q.Get("book_set"); v != "" {
+ b.WriteString(fmt.Sprintf("book_set=%s&", url.QueryEscape(v)))
+ }
+ if v := q.Get("onenum_mbooks"); v != "" {
+ b.WriteString(fmt.Sprintf("onenum_mbooks=%s&", url.QueryEscape(v)))
+ }
+ if v := q.Get("ill_publisher"); v != "" {
+ b.WriteString(fmt.Sprintf("ill_publisher=%s&", url.QueryEscape(v)))
+ }
+ b.WriteString(add("saleSelect"))
+ b.WriteString(add("category"))
+ if v := q.Get("ill_author"); v != "" {
+ b.WriteString(fmt.Sprintf("ill_author=%s&", url.QueryEscape(v)))
+ }
+ b.WriteString(add("publiction_times"))
+ b.WriteString(add("buy_counts"))
+ // sell_counts 最后一个不加 &
+ if v := strings.TrimSpace(q.Get("sell_counts")); v != "" {
+ b.WriteString(fmt.Sprintf("sell_counts=%s", url.QueryEscape(v)))
+ }
+ // 去除可能的尾部 &
+ u := b.String()
+ if strings.HasSuffix(u, "&") {
+ u = strings.TrimSuffix(u, "&")
+ }
+ return u
+}
+
+// buildTaskMapList 构造 taskMapList(无数据库,仅透传 shopIds 形成占位)
+// 兼容 Java 的数组元素形态:{ "taskId": "", "shopId": "", "shopType": "" }
+func buildTaskMapList(q url.Values) []map[string]string {
+ ids := strings.TrimSpace(q.Get("shopIds"))
+ types := q["shopType"] // 可选:与 shopIds 对应
+ if ids == "" {
+ return []map[string]string{}
+ }
+ idList := strings.Split(ids, ",")
+ out := make([]map[string]string, 0, len(idList))
+ for i, id := range idList {
+ m := map[string]string{
+ "taskId": "", // 无任务系统,留空
+ "shopId": strings.TrimSpace(id),
+ "shopType": "",
+ }
+ if i < len(types) {
+ m["shopType"] = strings.TrimSpace(types[i])
+ }
+ out = append(out, m)
+ }
+ return out
+}
+
+// writeJSON 写出 JSON 响应
+func writeJSON(w http.ResponseWriter, status int, v any) {
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
+ w.WriteHeader(status)
+ _ = json.NewEncoder(w).Encode(v)
+}
+
+// atoi 安全转换
+func atoi(s string) int {
+ v, _ := strconv.Atoi(strings.TrimSpace(s))
+ return v
+}
diff --git a/sql_monitor.go b/sql_monitor.go
new file mode 100644
index 0000000..5943230
--- /dev/null
+++ b/sql_monitor.go
@@ -0,0 +1,301 @@
+package main
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "sync"
+ "time"
+)
+
+// SQLExecutionRecord SQL执行记录
+type SQLExecutionRecord struct {
+ ID int64 `json:"id"`
+ Query string `json:"query"`
+ Duration int64 `json:"duration_ms"` // 执行时间(毫秒)
+ Timestamp time.Time `json:"timestamp"` // 执行时间戳
+ Success bool `json:"success"` // 是否成功
+ Error string `json:"error,omitempty"` // 错误信息
+ Endpoint string `json:"endpoint"` // 调用的接口
+ RowsAffected int64 `json:"rows_affected"` // 影响的行数
+}
+
+// SQLMonitor SQL监控器
+type SQLMonitor struct {
+ records []SQLExecutionRecord
+ mutex sync.RWMutex
+ maxSize int
+ nextID int64
+}
+
+// 全局SQL监控器实例
+var globalSQLMonitor *SQLMonitor
+
+// InitSQLMonitor 初始化SQL监控器
+func InitSQLMonitor(maxSize int) {
+ globalSQLMonitor = &SQLMonitor{
+ records: make([]SQLExecutionRecord, 0, maxSize),
+ maxSize: maxSize,
+ nextID: 1,
+ }
+ log.Printf("SQL监控器已初始化,最大记录数: %d", maxSize)
+}
+
+// ExecuteWithMonitor 执行SQL并监控性能
+func (sm *SQLMonitor) ExecuteWithMonitor(db *sql.DB, query string, endpoint string, args ...interface{}) (*sql.Rows, error) {
+ startTime := time.Now()
+
+ rows, err := db.Query(query, args...)
+
+ duration := time.Since(startTime)
+
+ // 记录执行信息
+ record := SQLExecutionRecord{
+ ID: sm.getNextID(),
+ Query: query,
+ Duration: duration.Milliseconds(),
+ Timestamp: startTime,
+ Success: err == nil,
+ Endpoint: endpoint,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ }
+
+ sm.addRecord(record)
+
+ // 打印SQL执行日志
+ if err != nil {
+ log.Printf("[SQL监控] 执行失败 - 接口: %s, 耗时: %dms, 错误: %v", endpoint, duration.Milliseconds(), err)
+ } else {
+ log.Printf("[SQL监控] 执行成功 - 接口: %s, 耗时: %dms", endpoint, duration.Milliseconds())
+ }
+
+ return rows, err
+}
+
+// ExecuteRowWithMonitor 执行单行查询并监控性能
+func (sm *SQLMonitor) ExecuteRowWithMonitor(db *sql.DB, query string, endpoint string, args ...interface{}) *sql.Row {
+ startTime := time.Now()
+
+ row := db.QueryRow(query, args...)
+
+ duration := time.Since(startTime)
+
+ // 记录执行信息
+ record := SQLExecutionRecord{
+ ID: sm.getNextID(),
+ Query: query,
+ Duration: duration.Milliseconds(),
+ Timestamp: startTime,
+ Success: true, // QueryRow总是成功,错误在Scan时才出现
+ Endpoint: endpoint,
+ }
+
+ sm.addRecord(record)
+
+ log.Printf("[SQL监控] QueryRow执行 - 接口: %s, 耗时: %dms", endpoint, duration.Milliseconds())
+
+ return row
+}
+
+// ExecuteExecWithMonitor 执行更新/插入/删除并监控性能
+func (sm *SQLMonitor) ExecuteExecWithMonitor(db *sql.DB, query string, endpoint string, args ...interface{}) (sql.Result, error) {
+ startTime := time.Now()
+
+ result, err := db.Exec(query, args...)
+
+ duration := time.Since(startTime)
+
+ // 记录执行信息
+ record := SQLExecutionRecord{
+ ID: sm.getNextID(),
+ Query: query,
+ Duration: duration.Milliseconds(),
+ Timestamp: startTime,
+ Success: err == nil,
+ Endpoint: endpoint,
+ }
+
+ if err != nil {
+ record.Error = err.Error()
+ } else if result != nil {
+ if rowsAffected, rowErr := result.RowsAffected(); rowErr == nil {
+ record.RowsAffected = rowsAffected
+ }
+ }
+
+ sm.addRecord(record)
+
+ // 打印SQL执行日志
+ if err != nil {
+ log.Printf("[SQL监控] Exec执行失败 - 接口: %s, 耗时: %dms, 错误: %v", endpoint, duration.Milliseconds(), err)
+ } else {
+ log.Printf("[SQL监控] Exec执行成功 - 接口: %s, 耗时: %dms, 影响行数: %d", endpoint, duration.Milliseconds(), record.RowsAffected)
+ }
+
+ return result, err
+}
+
+// getNextID 获取下一个ID
+func (sm *SQLMonitor) getNextID() int64 {
+ sm.mutex.Lock()
+ defer sm.mutex.Unlock()
+ id := sm.nextID
+ sm.nextID++
+ return id
+}
+
+// addRecord 添加记录
+func (sm *SQLMonitor) addRecord(record SQLExecutionRecord) {
+ sm.mutex.Lock()
+ defer sm.mutex.Unlock()
+
+ sm.records = append(sm.records, record)
+
+ // 保持最大记录数限制
+ if len(sm.records) > sm.maxSize {
+ sm.records = sm.records[1:]
+ }
+}
+
+// GetRecentRecords 获取最近的记录
+func (sm *SQLMonitor) GetRecentRecords(limit int) []SQLExecutionRecord {
+ sm.mutex.RLock()
+ defer sm.mutex.RUnlock()
+
+ if limit <= 0 || limit > len(sm.records) {
+ limit = len(sm.records)
+ }
+
+ start := len(sm.records) - limit
+ result := make([]SQLExecutionRecord, limit)
+ copy(result, sm.records[start:])
+
+ // 反转数组,最新的在前面
+ for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
+ result[i], result[j] = result[j], result[i]
+ }
+
+ return result
+}
+
+// GetStats 获取统计信息
+func (sm *SQLMonitor) GetStats() map[string]interface{} {
+ sm.mutex.RLock()
+ defer sm.mutex.RUnlock()
+
+ if len(sm.records) == 0 {
+ return map[string]interface{}{
+ "total_queries": 0,
+ "success_queries": 0,
+ "failed_queries": 0,
+ "success_rate": 0.0,
+ "avg_duration_ms": 0,
+ "max_duration_ms": 0,
+ "min_duration_ms": 0,
+ }
+ }
+
+ var totalDuration int64
+ var successCount, failedCount int64
+ var maxDuration, minDuration int64
+
+ maxDuration = sm.records[0].Duration
+ minDuration = sm.records[0].Duration
+
+ for _, record := range sm.records {
+ totalDuration += record.Duration
+
+ if record.Success {
+ successCount++
+ } else {
+ failedCount++
+ }
+
+ if record.Duration > maxDuration {
+ maxDuration = record.Duration
+ }
+ if record.Duration < minDuration {
+ minDuration = record.Duration
+ }
+ }
+
+ totalQueries := int64(len(sm.records))
+ avgDuration := totalDuration / totalQueries
+ successRate := float64(successCount) / float64(totalQueries) * 100
+
+ return map[string]interface{}{
+ "total_queries": totalQueries,
+ "success_queries": successCount,
+ "failed_queries": failedCount,
+ "success_rate": fmt.Sprintf("%.2f", successRate),
+ "avg_duration_ms": avgDuration,
+ "max_duration_ms": maxDuration,
+ "min_duration_ms": minDuration,
+ "last_update": time.Now().Format("2006-01-02 15:04:05"),
+ }
+}
+
+// ClearRecords 清空记录
+func (sm *SQLMonitor) ClearRecords() {
+ sm.mutex.Lock()
+ defer sm.mutex.Unlock()
+
+ sm.records = make([]SQLExecutionRecord, 0, sm.maxSize)
+ sm.nextID = 1
+ log.Println("[SQL监控] 记录已清空")
+}
+
+// GetSlowQueries 获取慢查询(超过指定时间的查询)
+func (sm *SQLMonitor) GetSlowQueries(thresholdMs int64) []SQLExecutionRecord {
+ sm.mutex.RLock()
+ defer sm.mutex.RUnlock()
+
+ var slowQueries []SQLExecutionRecord
+ for _, record := range sm.records {
+ if record.Duration > thresholdMs {
+ slowQueries = append(slowQueries, record)
+ }
+ }
+
+ return slowQueries
+}
+
+// GetFailedQueries 获取失败的查询
+func (sm *SQLMonitor) GetFailedQueries() []SQLExecutionRecord {
+ sm.mutex.RLock()
+ defer sm.mutex.RUnlock()
+
+ var failedQueries []SQLExecutionRecord
+ for _, record := range sm.records {
+ if !record.Success {
+ failedQueries = append(failedQueries, record)
+ }
+ }
+
+ return failedQueries
+}
+
+// 便捷方法:直接使用全局监控器
+func MonitorQuery(db *sql.DB, query string, endpoint string, args ...interface{}) (*sql.Rows, error) {
+ if globalSQLMonitor == nil {
+ return db.Query(query, args...)
+ }
+ return globalSQLMonitor.ExecuteWithMonitor(db, query, endpoint, args...)
+}
+
+func MonitorQueryRow(db *sql.DB, query string, endpoint string, args ...interface{}) *sql.Row {
+ if globalSQLMonitor == nil {
+ return db.QueryRow(query, args...)
+ }
+ return globalSQLMonitor.ExecuteRowWithMonitor(db, query, endpoint, args...)
+}
+
+func MonitorExec(db *sql.DB, query string, endpoint string, args ...interface{}) (sql.Result, error) {
+ if globalSQLMonitor == nil {
+ return db.Exec(query, args...)
+ }
+ return globalSQLMonitor.ExecuteExecWithMonitor(db, query, endpoint, args...)
+}
diff --git a/tail/tail.go b/tail/tail.go
new file mode 100644
index 0000000..2898088
--- /dev/null
+++ b/tail/tail.go
@@ -0,0 +1,291 @@
+package tail
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "time"
+)
+
+// 请求结构体
+// PriceCheckRequest 价格检查请求体
+// 功能:用于向价格检查服务提交待查询的 ISBN 列表
+type PriceCheckRequest struct {
+ ISBNList []string `json:"isbn_list"`
+}
+
+// 响应结构体 - 根据实际响应调整
+// PriceCheckResponse 价格检查响应体
+// 功能:接收价格检查服务返回的数据,并以 RawMessage 处理 data,兼容 [] 与 {}
+type PriceCheckResponse struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ DataRaw json.RawMessage `json:"data"`
+}
+
+// 更精确的响应结构体定义
+// ISBNItem 每个条目为一个 ISBN 的信息映射
+type ISBNItem map[string]ISBNInfo
+
+// ISBNInfo 单个 ISBN 的详细信息
+type ISBNInfo struct {
+ Data []interface{} `json:"data"`
+ ISBN string `json:"isbn"`
+ OnSaleCount FlexInt `json:"onSaleCount"`
+}
+
+// 检查价格并获取onSaleCount的方法
+// CheckPriceAndGetOnSaleCount 批量查询在售数量
+// 入参:ISBN 列表
+// 返回:每个 ISBN 的在售数量映射;当远端返回 data:{} 时返回空 map
+func CheckPriceAndGetOnSaleCount(isbnList []string) (map[string]int, error) {
+ result, err := CheckPrice(isbnList)
+ log.Printf("Response: %+v", result)
+ if err != nil {
+ return nil, err
+ }
+
+ counts, _, err := parseOnSaleCounts(result.DataRaw)
+ if err != nil {
+ return nil, err
+ }
+ return counts, nil
+}
+
+// 原有的CheckPrice方法
+// CheckPrice 调用远程价格检查服务
+// 功能:请求在售信息,返回完整响应体以供进一步解析
+func CheckPrice(isbnList []string) (*PriceCheckResponse, error) {
+ url := "http://114.66.2.223:7842/api/check_price/get"
+ token := "2cbdddfc747b437683cdb633b8c556e2"
+
+ requestBody := PriceCheckRequest{
+ ISBNList: isbnList,
+ }
+
+ jsonData, err := json.Marshal(requestBody)
+ if err != nil {
+ return nil, fmt.Errorf("序列化请求参数失败: %v", err)
+ }
+
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
+ if err != nil {
+ return nil, fmt.Errorf("创建请求失败: %v", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("token", token)
+ req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
+
+ client := &http.Client{Timeout: 30 * time.Second}
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("请求失败: %v", err)
+ }
+ defer resp.Body.Close()
+
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("读取响应失败: %v", err)
+ }
+
+ var response PriceCheckResponse
+ if err = json.Unmarshal(body, &response); err != nil {
+ return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
+ }
+
+ return &response, nil
+}
+
+// 专门获取onSaleCount的简化方法
+// GetOnSaleCount 查询单个 ISBN 的在售数量
+// 入参:isbn(必填)
+// 返回:该 ISBN 的在售数量;当远端返回 data:{} 或未命中时返回 0 且不报错
+func GetOnSaleCount(isbn string) (int, error) {
+ isbnList := []string{isbn}
+
+ result, err := CheckPrice(isbnList)
+ log.Printf("Response: %+v", result)
+ if err != nil {
+ return 0, err
+ }
+
+ counts, empty, err := parseOnSaleCounts(result.DataRaw)
+ if err != nil {
+ return 0, err
+ }
+ if empty {
+ return 0, nil
+ }
+ if v, ok := counts[isbn]; ok {
+ return v, nil
+ }
+ // 未命中时返回 0,不视为错误
+ return 0, nil
+}
+
+// parseOnSaleCounts 解析在售数量
+// 功能:兼容远端返回的 data 类型既可能是 [],也可能是 {}
+// 返回:
+// - counts: 每个 ISBN 的在售数量
+// - empty: data 为空对象或空数组时为 true
+// - err: 解析失败错误
+func parseOnSaleCounts(raw json.RawMessage) (map[string]int, bool, error) {
+ counts := make(map[string]int)
+
+ // 尝试按数组解析
+ var arr []ISBNItem
+ if err := json.Unmarshal(raw, &arr); err == nil {
+ if len(arr) == 0 {
+ return counts, true, nil
+ }
+ for _, item := range arr {
+ for isbn, info := range item {
+ counts[isbn] = int(info.OnSaleCount)
+ }
+ }
+ return counts, false, nil
+ }
+
+ // 尝试按对象解析(可能为空对象 {})
+ var obj map[string]ISBNInfo
+ if err := json.Unmarshal(raw, &obj); err == nil {
+ if len(obj) == 0 {
+ return counts, true, nil
+ }
+ for isbn, info := range obj {
+ counts[isbn] = int(info.OnSaleCount)
+ }
+ return counts, false, nil
+ }
+
+ // 两种形态都不匹配时返回解析错误
+ return nil, false, fmt.Errorf("无法解析在售数量数据")
+}
+
+// FlexInt 可兼容字符串或数字的整型解析
+// 功能:用于解析远端返回的既可能是字符串又可能是数字的整型字段
+type FlexInt int
+
+// UnmarshalJSON 自定义反序列化
+// 兼容 "123" 或 123 两种形式,并忽略空字符串
+func (fi *FlexInt) UnmarshalJSON(data []byte) error {
+ // 尝试解析为数字
+ var asNum int
+ if err := json.Unmarshal(data, &asNum); err == nil {
+ *fi = FlexInt(asNum)
+ return nil
+ }
+ // 尝试解析为字符串
+ var asStr string
+ if err := json.Unmarshal(data, &asStr); err == nil {
+ if asStr == "" {
+ *fi = 0
+ return nil
+ }
+ // 去除可能的空格
+ // 注意:不引入 strconv 以保持依赖简单,改用 json.Number 方案
+ var num json.Number
+ if err := json.Unmarshal([]byte("\""+asStr+"\""), &num); err == nil {
+ n, err := num.Int64()
+ if err == nil {
+ *fi = FlexInt(int(n))
+ return nil
+ }
+ }
+ }
+ // 尝试使用通用 Number 解析
+ var num json.Number
+ if err := json.Unmarshal(data, &num); err == nil {
+ n, err := num.Int64()
+ if err == nil {
+ *fi = FlexInt(int(n))
+ return nil
+ }
+ }
+ return fmt.Errorf("FlexInt 解析失败")
+}
+
+// 注意:此文件作为库使用,不包含可执行入口
+
+// 请求结构
+type SalesRequest struct {
+ ISBNList []string `json:"isbn_list"`
+}
+
+// 响应结构(可根据需要扩展)
+type SalesResponse struct {
+ Code int `json:"code"`
+ Message string `json:"message"`
+ Data map[string]BookSales `json:"data"`
+}
+
+type BookSales struct {
+ ISBN string `json:"isbn"`
+ DaySale7 string `json:"day_sale_7"`
+ DaySale15 string `json:"day_sale_15"`
+ DaySale30 string `json:"day_sale_30"`
+ DaySale60 string `json:"day_sale_60"`
+ DaySale90 string `json:"day_sale_90"`
+ DaySale180 string `json:"day_sale_180"`
+ DaySale365 string `json:"day_sale_365"`
+ ThisYearSale string `json:"this_year_sale"`
+ LastYearSale string `json:"last_year_sale"`
+ Sale string `json:"sale"`
+ SoldOut1 string `json:"sold_out_1"`
+ SoldOut2 string `json:"sold_out_2"`
+ SoldOut3 string `json:"sold_out_3"`
+ SoldOut4 string `json:"sold_out_4"`
+ SoldOut5 string `json:"sold_out_5"`
+ SoldOut6 string `json:"sold_out_6"`
+ SoldOut7 string `json:"sold_out_7"`
+ SoldOut8 string `json:"sold_out_8"`
+ ShipmentCycle string `json:"shipment_cycle"`
+}
+
+func CheckSales(isbnList []string) (*SalesResponse, error) {
+ url := "http://114.66.2.223:7842/api/check_price/sales"
+
+ // 请求体
+ reqBody := SalesRequest{
+ ISBNList: isbnList,
+ }
+ bodyBytes, _ := json.Marshal(reqBody)
+
+ client := &http.Client{Timeout: 10 * time.Second}
+
+ req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
+ if err != nil {
+ return nil, err
+ }
+
+ // 设置头
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("token", "2cbdddfc747b437683cdb633b8c556e2")
+
+ // 发送请求
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ // 读取响应
+ respBytes, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ // 解析 JSON
+ var salesResp SalesResponse
+ err = json.Unmarshal(respBytes, &salesResp)
+ if err != nil {
+ return nil, err
+ }
+
+ return &salesResp, nil
+}
diff --git a/test_es_dll.go b/test_es_dll.go
new file mode 100644
index 0000000..f0e3dce
--- /dev/null
+++ b/test_es_dll.go
@@ -0,0 +1,318 @@
+package main
+
+//
+//import (
+// "centerBook/es"
+// "encoding/json"
+// "fmt"
+// "log"
+// "os"
+//)
+//
+//func printUsage() {
+// fmt.Println("es.dll 测试工具")
+// fmt.Println("================")
+// fmt.Println("使用方法:")
+// fmt.Println(" go run test_dll.go <命令> [参数...]")
+// fmt.Println()
+// fmt.Println("可用命令:")
+// fmt.Println(" list - 查询所有索引")
+// fmt.Println(" info - 获取所有索引的详细信息")
+// fmt.Println(" detail <索引名> - 获取单个索引的详细信息")
+// fmt.Println(" create <索引名> <映射JSON> - 创建索引")
+// fmt.Println(" delete <索引名> - 删除索引")
+// fmt.Println(" count <索引名> - 获取索引文档数量")
+// fmt.Println(" get <索引名> <文档ID> - 获取文档")
+// fmt.Println(" add <索引名> <文档ID> - 创建文档")
+// fmt.Println(" update <索引名> <文档ID> - 更新文档")
+// fmt.Println(" del_doc <索引名> <文档ID> - 删除文档")
+// fmt.Println(" search <索引名> <查询JSON> - 搜索文档")
+// fmt.Println()
+// fmt.Println("示例:")
+// fmt.Println(" go run test_dll.go list")
+// fmt.Println(" go run test_dll.go create my_index '{\"properties\":{\"title\":{\"type\":\"text\"}}}'")
+// fmt.Println(" go run test_dll.go add my_index doc1 '{\"title\":\"测试文档\"}'")
+// fmt.Println(" go run test_dll.go search my_index '{\"query\":{\"match_all\":{}}}'")
+//}
+//
+//func main() {
+// if len(os.Args) < 2 {
+// printUsage()
+// os.Exit(1)
+// }
+//
+// // 初始化DLL
+// dll, err := es.InitEsDLL()
+// if err != nil {
+// log.Fatalf("初始化DLL失败: %v", err)
+// }
+// defer dll.Close()
+//
+// command := os.Args[1]
+//
+// switch command {
+// case "list":
+// handleListAllIndices(dll)
+// case "info":
+// handleGetIndicesInfo(dll)
+// case "detail":
+// if len(os.Args) < 3 {
+// fmt.Println("错误: 请提供索引名")
+// os.Exit(1)
+// }
+// handleGetIndexDetail(dll, os.Args[2])
+// case "create":
+// if len(os.Args) < 4 {
+// fmt.Println("错误: 请提供索引名和映射JSON")
+// os.Exit(1)
+// }
+// handleCreateIndex(dll, os.Args[2], os.Args[3])
+// case "delete":
+// if len(os.Args) < 3 {
+// fmt.Println("错误: 请提供索引名")
+// os.Exit(1)
+// }
+// handleDeleteIndex(dll, os.Args[2])
+// case "count":
+// if len(os.Args) < 3 {
+// fmt.Println("错误: 请提供索引名")
+// os.Exit(1)
+// }
+// handleGetDocumentCount(dll, os.Args[2])
+// case "get":
+// if len(os.Args) < 4 {
+// fmt.Println("错误: 请提供索引名和文档ID")
+// os.Exit(1)
+// }
+// handleGetDocument(dll, os.Args[2], os.Args[3])
+// case "add":
+// if len(os.Args) < 5 {
+// fmt.Println("错误: 请提供索引名、文档ID和文档JSON")
+// os.Exit(1)
+// }
+// handleCreateDocument(dll, os.Args[2], os.Args[3], os.Args[4])
+// case "update":
+// if len(os.Args) < 5 {
+// fmt.Println("错误: 请提供索引名、文档ID和更新数据JSON")
+// os.Exit(1)
+// }
+// handleUpdateDocument(dll, os.Args[2], os.Args[3], os.Args[4])
+// case "del_doc":
+// if len(os.Args) < 4 {
+// fmt.Println("错误: 请提供索引名和文档ID")
+// os.Exit(1)
+// }
+// handleDeleteDocument(dll, os.Args[2], os.Args[3])
+// case "search":
+// if len(os.Args) < 4 {
+// fmt.Println("错误: 请提供索引名和查询JSON")
+// os.Exit(1)
+// }
+// handleSearchDocuments(dll, os.Args[2], os.Args[3])
+// case "test":
+// runDemo(dll)
+// default:
+// fmt.Printf("未知命令: %s\n", command)
+// printUsage()
+// os.Exit(1)
+// }
+//}
+//
+//func printJSONResult(result string) {
+// // 尝试格式化JSON输出
+// var data interface{}
+// if err := json.Unmarshal([]byte(result), &data); err == nil {
+// formatted, _ := json.MarshalIndent(data, "", " ")
+// fmt.Println(string(formatted))
+// } else {
+// fmt.Println(result)
+// }
+//}
+//
+//func handleListAllIndices(dll *es.EsDLL) {
+// result, err := dll.ListAllIndices()
+// if err != nil {
+// log.Fatalf("查询失败: %v", err)
+// }
+// fmt.Println("=== 查询所有索引 ===")
+// printJSONResult(result)
+//}
+//
+//func handleGetIndicesInfo(dll *es.EsDLL) {
+// result, err := dll.GetIndicesInfo()
+// if err != nil {
+// log.Fatalf("查询失败: %v", err)
+// }
+// fmt.Println("=== 所有索引详细信息 ===")
+// printJSONResult(result)
+//}
+//
+//func handleGetIndexDetail(dll *es.EsDLL, indexName string) {
+// result, err := dll.GetIndexDetail(indexName)
+// if err != nil {
+// log.Fatalf("查询失败: %v", err)
+// }
+// fmt.Printf("=== 索引 '%s' 详情 ===\n", indexName)
+// printJSONResult(result)
+//}
+//
+//func handleCreateIndex(dll *es.EsDLL, indexName, mapping string) {
+// result, err := dll.CreateIndex(indexName, mapping)
+// if err != nil {
+// log.Fatalf("创建索引失败: %v", err)
+// }
+// fmt.Printf("=== 创建索引 '%s' ===\n", indexName)
+// printJSONResult(result)
+//}
+//
+//func handleDeleteIndex(dll *es.EsDLL, indexName string) {
+// result, err := dll.DeleteIndex(indexName)
+// if err != nil {
+// log.Fatalf("删除索引失败: %v", err)
+// }
+// fmt.Printf("=== 删除索引 '%s' ===\n", indexName)
+// printJSONResult(result)
+//}
+//
+//func handleGetDocumentCount(dll *es.EsDLL, indexName string) {
+// result, err := dll.GetDocumentCount(indexName)
+// if err != nil {
+// log.Fatalf("获取文档数量失败: %v", err)
+// }
+// fmt.Printf("=== 索引 '%s' 文档数量 ===\n", indexName)
+// printJSONResult(result)
+//}
+//
+//func handleGetDocument(dll *es.EsDLL, indexName, docID string) {
+// result, err := dll.GetDocument(indexName, docID)
+// if err != nil {
+// log.Fatalf("获取文档失败: %v", err)
+// }
+// fmt.Printf("=== 获取文档 %s/%s ===\n", indexName, docID)
+// printJSONResult(result)
+//}
+//
+//func handleCreateDocument(dll *es.EsDLL, indexName, docID, doc string) {
+// result, err := dll.CreateDocument(indexName, docID, doc)
+// if err != nil {
+// log.Fatalf("创建文档失败: %v", err)
+// }
+// fmt.Printf("=== 创建文档 %s/%s ===\n", indexName, docID)
+// printJSONResult(result)
+//}
+//
+//func handleUpdateDocument(dll *es.EsDLL, indexName, docID, updateData string) {
+// result, err := dll.UpdateDocument(indexName, docID, updateData)
+// if err != nil {
+// log.Fatalf("更新文档失败: %v", err)
+// }
+// fmt.Printf("=== 更新文档 %s/%s ===\n", indexName, docID)
+// printJSONResult(result)
+//}
+//
+//func handleDeleteDocument(dll *es.EsDLL, indexName, docID string) {
+// result, err := dll.DeleteDocument(indexName, docID)
+// if err != nil {
+// log.Fatalf("删除文档失败: %v", err)
+// }
+// fmt.Printf("=== 删除文档 %s/%s ===\n", indexName, docID)
+// printJSONResult(result)
+//}
+//
+//func handleSearchDocuments(dll *es.EsDLL, indexName, query string) {
+// result, err := dll.SearchDocuments(indexName, query)
+// if err != nil {
+// log.Fatalf("搜索文档失败: %v", err)
+// }
+// fmt.Printf("=== 搜索索引 '%s' ===\n", indexName)
+// printJSONResult(result)
+//}
+//
+//func runDemo(dll *es.EsDLL) {
+// fmt.Println("=== 运行演示测试 ===")
+//
+// // 1. 查询所有索引
+// fmt.Println("\n1. 查询所有索引")
+// result, _ := dll.ListAllIndices()
+// printJSONResult(result)
+//
+// // 创建测试索引
+// testIndex := "demo_test_index"
+// // Elasticsearch 8.x 需要 mappings 包装
+// mapping := `{
+// "mappings": {
+// "properties": {
+// "title": {"type": "text"},
+// "author": {"type": "keyword"},
+// "price": {"type": "double"},
+// "content": {"type": "text"}
+// }
+// }
+// }`
+//
+// // 2. 创建索引
+// fmt.Printf("\n2. 创建索引 '%s'\n", testIndex)
+// result, _ = dll.CreateIndex(testIndex, mapping)
+// printJSONResult(result)
+//
+// // 3. 添加文档
+// fmt.Println("\n3. 添加文档")
+// docs := []struct {
+// id string
+// json string
+// }{
+// {"book1", `{"title": "Go语言编程", "author": "张三", "price": 89.9, "content": "Go语言入门教程"}`},
+// {"book2", `{"title": "Python实战", "author": "李四", "price": 79.9, "content": "Python项目实战"}`},
+// {"book3", `{"title": "Go语言高级编程", "author": "王五", "price": 99.9, "content": "Go进阶内容"}`},
+// }
+//
+// for _, doc := range docs {
+// result, _ = dll.CreateDocument(testIndex, doc.id, doc.json)
+// fmt.Printf("添加文档 %s: ", doc.id)
+// printJSONResult(result)
+// }
+//
+// // 4. 获取文档数量
+// fmt.Printf("\n4. 获取索引 '%s' 文档数量\n", testIndex)
+// result, _ = dll.GetDocumentCount(testIndex)
+// printJSONResult(result)
+//
+// // 5. 获取单个文档
+// fmt.Println("\n5. 获取文档 book1")
+// result, _ = dll.GetDocument(testIndex, "book1")
+// printJSONResult(result)
+//
+// // 6. 搜索文档
+// fmt.Println("\n6.搜索包含'Go'的文档")
+// query := `{"query": {"match": {"title": "Go"}}}`
+// result, _ = dll.SearchDocuments(testIndex, query)
+// printJSONResult(result)
+//
+// // 7. 更新文档
+// fmt.Println("\n7. 更新文档 book1 的价格")
+// updateData := `{"doc": {"price": 85.0}}`
+// result, _ = dll.UpdateDocument(testIndex, "book1", updateData)
+// printJSONResult(result)
+//
+// // 8. 获取索引详情
+// fmt.Printf("\n8. 获取索引 '%s' 详情\n", testIndex)
+// result, _ = dll.GetIndexDetail(testIndex)
+// printJSONResult(result)
+//
+// // 9. 删除文档
+// fmt.Println("\n9. 删除文档 book3")
+// result, _ = dll.DeleteDocument(testIndex, "book3")
+// printJSONResult(result)
+//
+// // 10. 再次查询文档数量
+// fmt.Printf("\n10. 再次获取文档数量\n")
+// result, _ = dll.GetDocumentCount(testIndex)
+// printJSONResult(result)
+//
+// // 11. 清理:删除测试索引
+// fmt.Printf("\n11. 清理:删除测试索引 '%s'\n", testIndex)
+// result, _ = dll.DeleteIndex(testIndex)
+// printJSONResult(result)
+//
+// fmt.Println("\n=== 演示测试完成 ===")
+//}
diff --git a/util/dbClient/dbClient.go b/util/dbClient/dbClient.go
new file mode 100644
index 0000000..bcd33b1
--- /dev/null
+++ b/util/dbClient/dbClient.go
@@ -0,0 +1,40 @@
+package dbConnectUtil
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+
+ _ "github.com/go-sql-driver/mysql"
+)
+
+// DB 数据库连接池
+var DB *sql.DB
+
+// InitDB 初始化数据库连接
+func InitDB(username, password, host string, port int, dbName string) (*sql.DB, error) {
+ log.Printf("开始初始化数据库连接")
+ dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
+ username, password, host, port, dbName)
+
+ log.Printf("数据库连接参数: %s", dsn)
+ log.Print("开始连接数据库")
+
+ var err error
+ DB, err = sql.Open("mysql", dsn)
+ if err != nil {
+ return DB, fmt.Errorf("数据库连接失败: %v", err)
+ }
+
+ // 测试数据库连接
+ err = DB.Ping()
+ if err != nil {
+ return DB, fmt.Errorf("数据库连接测试失败: %v", err)
+ }
+
+ // 设置连接池参数
+ DB.SetMaxOpenConns(20)
+ DB.SetMaxIdleConns(10)
+
+ return DB, nil
+}
diff --git a/util/esClient/esClient.go b/util/esClient/esClient.go
new file mode 100644
index 0000000..31b5198
--- /dev/null
+++ b/util/esClient/esClient.go
@@ -0,0 +1,33 @@
+package esClient
+
+import (
+ "github.com/elastic/go-elasticsearch/v8"
+)
+
+type ESClient struct {
+ Client *elasticsearch.Client
+}
+
+func NewESClient(addresses []string, username, password string) (*ESClient, error) {
+ cfg := elasticsearch.Config{
+ Addresses: addresses,
+ Username: username,
+ Password: password,
+ }
+
+ client, err := elasticsearch.NewClient(cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ return &ESClient{
+ Client: client,
+ }, nil
+}
+
+// Close 关闭 Elasticsearch 客户端连接
+func (e *ESClient) Close() error {
+ // 目前 elasticsearch 客户端没有提供 Close 方法
+ // 这里可以添加一些清理工作
+ return nil
+}
diff --git a/util/pdd/pdd.go b/util/pdd/pdd.go
new file mode 100644
index 0000000..b233b01
--- /dev/null
+++ b/util/pdd/pdd.go
@@ -0,0 +1,192 @@
+package pdd
+
+/*
+#cgo LDFLAGS: -ldl
+#include
+#include
+#include
+
+// C包装函数,用于调用动态加载的函数
+static void* get_func(void* handle, const char* name) {
+ return dlsym(handle, name);
+}
+
+// 包装PddGoodsOuterCatMappingGet函数
+static char* call_PddGoodsOuterCatMappingGet(void* func, const char* clientId, const char* clientSecret,
+ const char* accessToken, const char* outerCatId, const char* outerCatName, const char* outerGoodsName) {
+ typedef char* (*FuncType)(const char*, const char*, const char*, const char*, const char*, const char*);
+ return ((FuncType)func)(clientId, clientSecret, accessToken, outerCatId, outerCatName, outerGoodsName);
+}
+
+// 包装FreeCString函数
+static void call_FreeCString(void* func, char* str) {
+ typedef void (*FuncType)(char*);
+ if (func != NULL && str != NULL) {
+ ((FuncType)func)(str);
+ }
+}
+*/
+import "C"
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+ "unsafe"
+)
+
+// PddResponse 定义完整的响应结构(包含成功和失败两种情况)
+type PddResponse struct {
+ SuccessResponse *PddCategoryMappingResponse `json:"outer_cat_mapping_get_response,omitempty"`
+ ErrorResponse *PddErrorResponse `json:"error_response,omitempty"`
+}
+
+// PddCategoryMappingResponse 定义拼多多API响应结构(根据文档规范)
+type PddCategoryMappingResponse struct {
+ CatID1 int64 `json:"cat_id1"` // 一级类目ID
+ CatID2 int64 `json:"cat_id2"` // 二级类目ID
+ CatID3 int64 `json:"cat_id3"` // 三级类目ID
+ CatID4 int64 `json:"cat_id4"` // 四级类目ID
+ RequestID string `json:"request_id"` // 请求ID
+}
+
+type PddSO struct {
+ handle unsafe.Pointer // 动态库句柄
+ pddGoodsOuterCatMappingGet unsafe.Pointer // 类目预测函数指针
+ freeCString unsafe.Pointer // 释放C字符串函数指针
+}
+type PddErrorResponse struct {
+ ErrorCode int64 `json:"error_code"` // 错误码
+ ErrorMsg string `json:"error_msg"` // 错误信息
+ SubCode *string `json:"sub_code"` // 子错误码
+ SubMsg string `json:"sub_msg"` // 子错误信息
+ RequestID string `json:"request_id"` // 请求ID
+}
+
+var (
+ instance *PddSO
+ once sync.Once
+)
+
+// GetPddInstance 获取拼多多客户端单例实例
+func GetPddInstance() (*PddSO, error) {
+ var initErr error
+ once.Do(func() {
+ instance, initErr = InitPddSO()
+ })
+ return instance, initErr
+}
+
+// InitPddSO 初始化pddSO
+func InitPddSO() (*PddSO, error) {
+ soPath := filepath.Join("so", "pdd.so")
+ if _, err := os.Stat(soPath); os.IsNotExist(err) {
+ return nil, fmt.Errorf("pdd SO 不存在: %s", soPath)
+ }
+
+ // 加载动态库
+ soPathC := C.CString(soPath)
+ defer C.free(unsafe.Pointer(soPathC))
+
+ handle := C.dlopen(soPathC, C.RTLD_LAZY)
+ if handle == nil {
+ errMsg := C.GoString(C.dlerror())
+ return nil, fmt.Errorf("加载pdd SO 失败: %s", errMsg)
+ }
+
+ // 获取函数指针
+ // 获取函数指针
+ getFunc := func(name string) (unsafe.Pointer, error) {
+ funcPtr := C.get_func(handle, C.CString(name))
+ if funcPtr == nil {
+ C.dlclose(handle)
+ return nil, fmt.Errorf("找不到函数 %s", name)
+ }
+ return funcPtr, nil
+ }
+
+ pddFunc, err := getFunc("PddGoodsOuterCatMappingGet")
+ if err != nil {
+ return nil, err
+ }
+
+ freeFunc, err := getFunc("FreeCString")
+ if err != nil {
+ return nil, err
+ }
+
+ return &PddSO{
+ handle: handle,
+ pddGoodsOuterCatMappingGet: pddFunc,
+ freeCString: freeFunc,
+ }, nil
+}
+
+// PddGoodsOuterCatMappingGet 类目检测
+func (m *PddSO) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken,
+ outerCatId, outerCatName, outerGoodsName string) (string, error) {
+
+ // 使用闭包简化C字符串管理
+ toCStr := func(s string) (*C.char, func()) {
+ cStr := C.CString(s)
+ return cStr, func() { C.free(unsafe.Pointer(cStr)) }
+ }
+
+ clientIdC, cleanup1 := toCStr(clientId)
+ defer cleanup1()
+
+ clientSecretC, cleanup2 := toCStr(clientSecret)
+ defer cleanup2()
+
+ accessTokenC, cleanup3 := toCStr(accessToken)
+ defer cleanup3()
+
+ outerCatIdC, cleanup4 := toCStr(outerCatId)
+ defer cleanup4()
+
+ outerCatNameC, cleanup5 := toCStr(outerCatName)
+ defer cleanup5()
+
+ outerGoodsNameC, cleanup6 := toCStr(outerGoodsName)
+ defer cleanup6()
+
+ result := C.call_PddGoodsOuterCatMappingGet(
+ m.pddGoodsOuterCatMappingGet,
+ clientIdC, clientSecretC, accessTokenC,
+ outerCatIdC, outerCatNameC, outerGoodsNameC,
+ )
+
+ if result == nil {
+ return "", fmt.Errorf("调用PddGoodsOuterCatMappingGet失败")
+ }
+ defer C.call_FreeCString(m.freeCString, result)
+
+ return m.parseCategoryMappingResponse(C.GoString(result))
+}
+
+// parseCategoryMappingResponse 解析类目映射API响应
+func (m *PddSO) parseCategoryMappingResponse(result string) (string, error) {
+ var apiResponse PddResponse
+ if err := json.Unmarshal([]byte(result), &apiResponse); err != nil {
+ return "", fmt.Errorf("JSON解析失败: %v, 原始数据: %s", err, result)
+ }
+
+ // 判断是成功响应还是错误响应
+ switch {
+ case apiResponse.ErrorResponse != nil:
+ errorResp := apiResponse.ErrorResponse
+ return "", fmt.Errorf("API调用失败 - 错误码: %d, 错误信息: %s",
+ errorResp.ErrorCode, errorResp.ErrorMsg)
+
+ case apiResponse.SuccessResponse != nil:
+ successResp := apiResponse.SuccessResponse
+ return fmt.Sprintf("%d/%d/%d",
+ successResp.CatID1,
+ successResp.CatID2,
+ successResp.CatID3), nil
+
+ default:
+ return "", fmt.Errorf("未知的响应格式: %s", result)
+ }
+}
diff --git a/util/redisClient/redisClient.go b/util/redisClient/redisClient.go
new file mode 100644
index 0000000..5176f12
--- /dev/null
+++ b/util/redisClient/redisClient.go
@@ -0,0 +1,105 @@
+package redisClient
+
+import (
+ "errors"
+ "github.com/go-redis/redis/v8"
+ "sync"
+)
+
+// 保持向后兼容的全局客户端
+var Client *redis.Client
+
+// 连接管理器,用于管理多个 Redis 连接
+var (
+ clients = make(map[string]*redis.Client)
+ clientsMux sync.RWMutex
+)
+
+// InitRedis 初始化默认 Redis 客户端(保持向后兼容)
+func InitRedis(addr, password string, db int) {
+ Client = redis.NewClient(&redis.Options{
+ Addr: addr,
+ Password: password,
+ DB: db,
+ })
+ // 同时将默认客户端添加到连接管理器
+ clientsMux.Lock()
+ clients["default"] = Client
+ clientsMux.Unlock()
+}
+
+// GetClient 获取默认 Redis 客户端(保持向后兼容)
+func GetClient() *redis.Client {
+ return Client
+}
+
+// CloseRedis 关闭默认 Redis 客户端(保持向后兼容)
+func CloseRedis() error {
+ if Client != nil {
+ return Client.Close()
+ }
+ return nil
+}
+
+// AddClient 添加一个新的 Redis 客户端
+func AddClient(name, addr, password string, db int) {
+ client := redis.NewClient(&redis.Options{
+ Addr: addr,
+ Password: password,
+ DB: db,
+ })
+ clientsMux.Lock()
+ clients[name] = client
+ clientsMux.Unlock()
+}
+
+// GetClientByName 根据名称获取 Redis 客户端
+func GetClientByName(name string) (*redis.Client, error) {
+ clientsMux.RLock()
+ defer clientsMux.RUnlock()
+ client, ok := clients[name]
+ if !ok {
+ return nil, errors.New("redis client not found: " + name)
+ }
+ return client, nil
+}
+
+// CloseClient 关闭指定名称的 Redis 客户端
+func CloseClient(name string) error {
+ clientsMux.Lock()
+ defer clientsMux.Unlock()
+ client, ok := clients[name]
+ if !ok {
+ return errors.New("redis client not found: " + name)
+ }
+ err := client.Close()
+ if err == nil {
+ delete(clients, name)
+ }
+ return err
+}
+
+// CloseAllClients 关闭所有 Redis 客户端
+func CloseAllClients() error {
+ clientsMux.Lock()
+ defer clientsMux.Unlock()
+ var lastErr error
+ for name, client := range clients {
+ if err := client.Close(); err != nil {
+ lastErr = err
+ }
+ delete(clients, name)
+ }
+ return lastErr
+}
+
+// ListClients 列出所有可用的 Redis 客户端名称
+func ListClients() []string {
+ clientsMux.RLock()
+ defer clientsMux.RUnlock()
+ names := make([]string, 0, len(clients))
+ for name := range clients {
+ names = append(names, name)
+ }
+ return names
+}
diff --git a/win/go_build_centerBook.exe b/win/go_build_centerBook.exe
new file mode 100644
index 0000000..0ec0766
Binary files /dev/null and b/win/go_build_centerBook.exe differ
diff --git a/win/go_build_centerBook_windows_GOARCH_amd64 b/win/go_build_centerBook_windows_GOARCH_amd64
new file mode 100644
index 0000000..bb7da68
--- /dev/null
+++ b/win/go_build_centerBook_windows_GOARCH_amd64
@@ -0,0 +1 @@
+ELF
\ No newline at end of file
diff --git a/win/go_build_win_centerBook.exe b/win/go_build_win_centerBook.exe
new file mode 100644
index 0000000..083e40c
Binary files /dev/null and b/win/go_build_win_centerBook.exe differ