初始化

This commit is contained in:
97694731 2026-02-28 14:27:33 +08:00
parent b75afd13e9
commit 9c9692f8d7
45 changed files with 15387 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

10
.idea/AugmentWebviewStateStore.xml generated Normal file

File diff suppressed because one or more lines are too long

9
.idea/centerBook.iml generated Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="UsePropertyAccessSyntax" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/centerBook.iml" filepath="$PROJECT_DIR$/.idea/centerBook.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

58
controller/pddCatId.go Normal file
View File

@ -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()
}

89
erp/erp_api.go Normal file
View File

@ -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))
}

307
es/DLL测试说明.md Normal file
View File

@ -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 字符串正确释放
- [ ] 资源正确释放

51
es/es_client.go Normal file
View File

@ -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
}

265
es/es_dll.go Normal file
View File

@ -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
//}

3740
es/es_search.go Normal file

File diff suppressed because it is too large Load Diff

8
es_dll/.idea/es_dll.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
es_dll/.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/es_dll.iml" filepath="$PROJECT_DIR$/.idea/es_dll.iml" />
</modules>
</component>
</project>

6
es_dll/.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

61
es_dll/.idea/workspace.xml generated Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="71764b10-af60-4674-ad57-ddfb8a6800b7" name="更改" comment="">
<change beforePath="$PROJECT_DIR$/../es/es_search.go" beforeDir="false" afterPath="$PROJECT_DIR$/../es/es_search.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../main.go" beforeDir="false" afterPath="$PROJECT_DIR$/../main.go" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 4
}]]></component>
<component name="ProjectId" id="39Tfc4teG0UbweodRDjtjFab156" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.GoLinterPluginOnboarding": "true",
"RunOnceActivity.GoLinterPluginStorageMigration": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"git-widget-placeholder": "master",
"last_opened_file_path": "D:/project/centerbook/es_dll",
"nodejs_package_manager_path": "npm",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-gosdk-3b128438d3f6-4b567d62c776-org.jetbrains.plugins.go.sharedIndexes.bundled-GO-251.26094.127" />
<option value="bundled-js-predefined-d6986cc7102b-b26f3e71634d-JavaScript-GO-251.26094.127" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="71764b10-af60-4674-ad57-ddfb8a6800b7" name="更改" comment="" />
<created>1770725057358</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1770725057358</updated>
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>
</component>
</project>

BIN
es_dll/es.dll Normal file

Binary file not shown.

346
es_dll/es.md Normal file
View File

@ -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": "更多内容"
}
]
}
```

57
go.mod Normal file
View File

@ -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
)

150
go.sum Normal file
View File

@ -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=

393
health_api.go Normal file
View File

@ -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 := `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SQL健康监控仪表板</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; }
.card { background: white; border-radius: 8px; padding: 20px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }
.stat-item { text-align: center; padding: 15px; background: #f8f9fa; border-radius: 6px; }
.stat-value { font-size: 24px; font-weight: bold; color: #007bff; }
.stat-label { font-size: 14px; color: #666; margin-top: 5px; }
.success { color: #28a745; }
.warning { color: #ffc107; }
.danger { color: #dc3545; }
.btn { padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; margin: 5px; }
.btn:hover { background: #0056b3; }
.btn-danger { background: #dc3545; }
.btn-danger:hover { background: #c82333; }
.table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 12px; }
.table th, .table td { padding: 6px; text-align: left; border-bottom: 1px solid #ddd; }
.table th { background-color: #f8f9fa; font-weight: bold; }
.query-text { max-width: 400px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: monospace; }
.auto-refresh { margin: 10px 0; }
.controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.filter-input { padding: 5px; border: 1px solid #ddd; border-radius: 4px; }
</style>
</head>
<body>
<div class="container">
<h1>SQL健康监控仪表板</h1>
<div class="controls">
<label>
<input type="checkbox" id="autoRefresh" checked> 自动刷新 (10)
</label>
<button class="btn" onclick="refreshData()">立即刷新</button>
<button class="btn btn-danger" onclick="clearRecords()">清空记录</button>
<input type="number" id="limitInput" class="filter-input" placeholder="记录数量" value="50" min="1" max="500">
<button class="btn" onclick="updateLimit()">更新数量</button>
</div>
<div class="card">
<h2>SQL执行统计</h2>
<div class="stats-grid" id="statsGrid">
<!-- 统计数据将在这里动态加载 -->
</div>
</div>
<div class="card">
<h2>最近SQL执行记录</h2>
<div id="recentRecords">
<!-- SQL记录将在这里动态加载 -->
</div>
</div>
<div class="card">
<h2>慢查询 (>1000ms)</h2>
<div id="slowQueries">
<!-- 慢查询将在这里动态加载 -->
</div>
</div>
<div class="card">
<h2>失败查询</h2>
<div id="failedQueries">
<!-- 失败查询将在这里动态加载 -->
</div>
</div>
</div>
<script>
let autoRefreshInterval;
let currentLimit = 50;
function refreshData() {
loadStats();
loadRecentRecords();
loadSlowQueries();
loadFailedQueries();
}
function loadStats() {
fetch('/api/sql-health/stats')
.then(response => response.json())
.then(data => {
const stats = data.data;
const statsGrid = document.getElementById('statsGrid');
statsGrid.innerHTML = ` + "`" + `
<div class="stat-item">
<div class="stat-value">${stats.total_queries}</div>
<div class="stat-label">总查询数</div>
</div>
<div class="stat-item">
<div class="stat-value success">${stats.success_queries}</div>
<div class="stat-label">成功查询</div>
</div>
<div class="stat-item">
<div class="stat-value danger">${stats.failed_queries}</div>
<div class="stat-label">失败查询</div>
</div>
<div class="stat-item">
<div class="stat-value ${parseFloat(stats.success_rate) >= 95 ? 'success' : parseFloat(stats.success_rate) >= 90 ? 'warning' : 'danger'}">${stats.success_rate}%</div>
<div class="stat-label">成功率</div>
</div>
<div class="stat-item">
<div class="stat-value">${stats.avg_duration_ms}ms</div>
<div class="stat-label">平均耗时</div>
</div>
<div class="stat-item">
<div class="stat-value">${stats.max_duration_ms}ms</div>
<div class="stat-label">最大耗时</div>
</div>
<div class="stat-item">
<div class="stat-value">${stats.min_duration_ms}ms</div>
<div class="stat-label">最小耗时</div>
</div>
` + "`" + `;
})
.catch(error => console.error('加载统计数据失败:', error));
}
function loadRecentRecords() {
fetch(` + "`" + `/api/sql-health/recent?limit=${currentLimit}` + "`" + `)
.then(response => response.json())
.then(data => {
const records = data.data.records;
const container = document.getElementById('recentRecords');
if (!records || records.length === 0) {
container.innerHTML = '<p>暂无记录</p>';
return;
}
let html = '<table class="table"><thead><tr><th>ID</th><th>查询语句</th><th>耗时(ms)</th><th>时间</th><th>接口</th><th>状态</th><th>影响行数</th></tr></thead><tbody>';
records.forEach(record => {
html += ` + "`" + `<tr>
<td>${record.id}</td>
<td class="query-text" title="${record.query}">${record.query}</td>
<td class="${record.duration_ms > 1000 ? 'danger' : record.duration_ms > 500 ? 'warning' : ''}">${record.duration_ms}</td>
<td>${new Date(record.timestamp).toLocaleString()}</td>
<td>${record.endpoint}</td>
<td class="${record.success ? 'success' : 'danger'}">${record.success ? '成功' : '失败'}</td>
<td>${record.rows_affected || 0}</td>
</tr>` + "`" + `;
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(error => console.error('加载记录失败:', error));
}
function loadSlowQueries() {
fetch('/api/sql-health/slow-queries?threshold=1000')
.then(response => response.json())
.then(data => {
const slowQueries = data.data.slow_queries;
const container = document.getElementById('slowQueries');
if (!slowQueries || slowQueries.length === 0) {
container.innerHTML = '<p>暂无慢查询</p>';
return;
}
let html = '<table class="table"><thead><tr><th>ID</th><th>查询语句</th><th>耗时(ms)</th><th>时间</th><th>接口</th></tr></thead><tbody>';
slowQueries.forEach(query => {
html += ` + "`" + `<tr>
<td>${query.id}</td>
<td class="query-text" title="${query.query}">${query.query}</td>
<td class="danger">${query.duration_ms}</td>
<td>${new Date(query.timestamp).toLocaleString()}</td>
<td>${query.endpoint}</td>
</tr>` + "`" + `;
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(error => console.error('加载慢查询失败:', error));
}
function loadFailedQueries() {
fetch('/api/sql-health/failed-queries')
.then(response => response.json())
.then(data => {
const failedQueries = data.data.failed_queries;
const container = document.getElementById('failedQueries');
if (!failedQueries || failedQueries.length === 0) {
container.innerHTML = '<p>暂无失败查询</p>';
return;
}
let html = '<table class="table"><thead><tr><th>ID</th><th>查询语句</th><th>错误信息</th><th>时间</th><th>接口</th></tr></thead><tbody>';
failedQueries.forEach(query => {
html += ` + "`" + `<tr>
<td>${query.id}</td>
<td class="query-text" title="${query.query}">${query.query}</td>
<td class="danger">${query.error}</td>
<td>${new Date(query.timestamp).toLocaleString()}</td>
<td>${query.endpoint}</td>
</tr>` + "`" + `;
});
html += '</tbody></table>';
container.innerHTML = html;
})
.catch(error => console.error('加载失败查询失败:', error));
}
function clearRecords() {
if (confirm('确定要清空所有SQL记录吗')) {
fetch('/api/sql-health/clear', { method: 'POST' })
.then(response => response.json())
.then(data => {
alert('记录已清空');
refreshData();
})
.catch(error => console.error('清空记录失败:', error));
}
}
function updateLimit() {
const limitInput = document.getElementById('limitInput');
const newLimit = parseInt(limitInput.value);
if (newLimit > 0 && newLimit <= 500) {
currentLimit = newLimit;
loadRecentRecords();
} else {
alert('记录数量必须在1-500之间');
}
}
function toggleAutoRefresh() {
const checkbox = document.getElementById('autoRefresh');
if (checkbox.checked) {
autoRefreshInterval = setInterval(refreshData, 10000);
} else {
clearInterval(autoRefreshInterval);
}
}
// 初始化
document.getElementById('autoRefresh').addEventListener('change', toggleAutoRefresh);
refreshData();
toggleAutoRefresh();
</script>
</body>
</html>
`
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusOK, dashboardHTML)
}

139
image/image.go Normal file
View File

@ -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)
}

79
ip_log.go Normal file
View File

@ -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.txtLinux 默认:/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))
}
}

109
kongfz/kongfz.go Normal file
View File

@ -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)
}

BIN
linux/goCenterBook Normal file

Binary file not shown.

BIN
linux/goCenterBook_linux Normal file

Binary file not shown.

BIN
linux/linux_linux Normal file

Binary file not shown.

54
logging_middleware.go Normal file
View File

@ -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 = "<NoRoute>"
}
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(),
})
}

5662
main.go Normal file

File diff suppressed because it is too large Load Diff

664
md/API.md Normal file
View File

@ -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

817
md/PROJECT_GUIDE.md Normal file
View File

@ -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 <repository-url>
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

336
md/README.md Normal file
View File

@ -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 <repository-url>
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 */;

252
md/sql_monitor_usage.md Normal file
View File

@ -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. 制定扩容计划

79
middleware/middleware.go Normal file
View File

@ -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": "请求过于频繁,请稍后再试",
})
}
}

329
pricing.go Normal file
View File

@ -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": "<id>", "shopType": "<type>" }
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
}

301
sql_monitor.go Normal file
View File

@ -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...)
}

291
tail/tail.go Normal file
View File

@ -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
}

318
test_es_dll.go Normal file
View File

@ -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> <JSON> - 创建文档")
// fmt.Println(" update <索引名> <文档ID> <JSON> - 更新文档")
// 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=== 演示测试完成 ===")
//}

40
util/dbClient/dbClient.go Normal file
View File

@ -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
}

33
util/esClient/esClient.go Normal file
View File

@ -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
}

192
util/pdd/pdd.go Normal file
View File

@ -0,0 +1,192 @@
package pdd
/*
#cgo LDFLAGS: -ldl
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
// 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)
}
}

View File

@ -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
}

BIN
win/go_build_centerBook.exe Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
ELF

Binary file not shown.