初始化
This commit is contained in:
parent
b75afd13e9
commit
9c9692f8d7
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal 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
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
9
.idea/centerBook.iml
generated
Normal 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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
58
controller/pddCatId.go
Normal 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
89
erp/erp_api.go
Normal 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
307
es/DLL测试说明.md
Normal 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
51
es/es_client.go
Normal 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
265
es/es_dll.go
Normal 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
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
8
es_dll/.idea/es_dll.iml
generated
Normal 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
8
es_dll/.idea/modules.xml
generated
Normal 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
6
es_dll/.idea/vcs.xml
generated
Normal 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
61
es_dll/.idea/workspace.xml
generated
Normal 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
BIN
es_dll/es.dll
Normal file
Binary file not shown.
346
es_dll/es.md
Normal file
346
es_dll/es.md
Normal 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
57
go.mod
Normal 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
150
go.sum
Normal 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
393
health_api.go
Normal 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
139
image/image.go
Normal 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
79
ip_log.go
Normal 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.txt;Linux 默认:/www/wwwroot/centerBook/ip.txt。
|
||||
func EnsureIPLogPathInitialized() {
|
||||
if ipLogFilePath != "" {
|
||||
return
|
||||
}
|
||||
if env := os.Getenv("IP_LOG_FILE"); env != "" {
|
||||
ipLogFilePath = env
|
||||
} else {
|
||||
if runtime.GOOS == "windows" {
|
||||
ipLogFilePath = `C:\Users\www\centerBook\ip.txt`
|
||||
} else {
|
||||
ipLogFilePath = "/www/wwwroot/centerBook/ip.txt"
|
||||
}
|
||||
}
|
||||
dir := filepath.Dir(ipLogFilePath)
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
// LogLargePerPage 记录超大 per_page 请求的IP信息到文本文件。
|
||||
// 触发条件:当 per_page 被裁剪(原始值大于上限)时调用。
|
||||
// 记录内容:时间、IP、方法、原始per_page、裁剪后per_page、路径、UA。
|
||||
func LogLargePerPage(c *gin.Context, originalPerPage, sanitizedPerPage int) {
|
||||
EnsureIPLogPathInitialized()
|
||||
line := fmt.Sprintf(
|
||||
"%s | ip=%s | method=%s | per_page=%d->%d | path=%s | ua=%s\n",
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
c.ClientIP(),
|
||||
c.Request.Method,
|
||||
originalPerPage,
|
||||
sanitizedPerPage,
|
||||
c.Request.RequestURI,
|
||||
strings.ReplaceAll(c.GetHeader("User-Agent"), "\n", " "),
|
||||
)
|
||||
ipLogMu.Lock()
|
||||
defer ipLogMu.Unlock()
|
||||
f, err := os.OpenFile(ipLogFilePath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
// 静默失败,避免影响主流程
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, _ = f.WriteString(line)
|
||||
}
|
||||
|
||||
// IPLogHandler 返回 ip.txt 的内容(text/plain)。
|
||||
// 用途:通过路由暴露 ip.txt,便于反向代理将 https://book.center.buzhiyushu.cn/ip.txt 映射到此接口。
|
||||
func IPLogHandler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
EnsureIPLogPathInitialized()
|
||||
data, err := os.ReadFile(ipLogFilePath)
|
||||
if err != nil {
|
||||
c.String(200, "")
|
||||
return
|
||||
}
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.String(200, string(data))
|
||||
}
|
||||
}
|
||||
109
kongfz/kongfz.go
Normal file
109
kongfz/kongfz.go
Normal 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
BIN
linux/goCenterBook
Normal file
Binary file not shown.
BIN
linux/goCenterBook_linux
Normal file
BIN
linux/goCenterBook_linux
Normal file
Binary file not shown.
BIN
linux/linux_linux
Normal file
BIN
linux/linux_linux
Normal file
Binary file not shown.
54
logging_middleware.go
Normal file
54
logging_middleware.go
Normal 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(),
|
||||
})
|
||||
}
|
||||
664
md/API.md
Normal file
664
md/API.md
Normal 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
817
md/PROJECT_GUIDE.md
Normal 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
336
md/README.md
Normal 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
252
md/sql_monitor_usage.md
Normal 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
79
middleware/middleware.go
Normal 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
329
pricing.go
Normal 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
301
sql_monitor.go
Normal 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
291
tail/tail.go
Normal 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
318
test_es_dll.go
Normal 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
40
util/dbClient/dbClient.go
Normal 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
33
util/esClient/esClient.go
Normal 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
192
util/pdd/pdd.go
Normal 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)
|
||||
}
|
||||
}
|
||||
105
util/redisClient/redisClient.go
Normal file
105
util/redisClient/redisClient.go
Normal 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
BIN
win/go_build_centerBook.exe
Normal file
Binary file not shown.
1
win/go_build_centerBook_windows_GOARCH_amd64
Normal file
1
win/go_build_centerBook_windows_GOARCH_amd64
Normal file
@ -0,0 +1 @@
|
||||
ELF
|
||||
BIN
win/go_build_win_centerBook.exe
Normal file
BIN
win/go_build_win_centerBook.exe
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user