diff --git a/csv/csv.go b/csv/csv.go index b8e4b8c..c965d73 100644 --- a/csv/csv.go +++ b/csv/csv.go @@ -181,7 +181,7 @@ func (mgr *CSVManager) OpenCSVFile(filename string, delimiter rune, hasHeader bo } // 获取总行数 - rows, err := csvHandle.GetTotalRows() + rows, err := csvHandle.getTotalRows() if err != nil { return -1, err } @@ -190,7 +190,7 @@ func (mgr *CSVManager) OpenCSVFile(filename string, delimiter rune, hasHeader bo // 读取表头(如果需要) if hasHeader { if err := mgr.readHeader(csvHandle); err != nil { - csvHandle.Close() + csvHandle.close() return -1, fmt.Errorf("读取表头失败: %w", err) } } @@ -206,7 +206,7 @@ func (mgr *CSVManager) OpenCSVFile(filename string, delimiter rune, hasHeader bo return handleID, nil } -// openFile 打开文件 +// 打开文件 func (mgr *CSVManager) openFile(handle *CSVHandle) error { handle.mu.Lock() defer handle.mu.Unlock() @@ -222,6 +222,7 @@ func (mgr *CSVManager) openFile(handle *CSVHandle) error { } fileSize := fileInfo.Size() + fmt.Println("文件大小:", fileSize) // 根据文件大小选择打开策略 if mgr.config.UseMMap && fileSize > mgr.config.MMapThreshold { @@ -231,7 +232,7 @@ func (mgr *CSVManager) openFile(handle *CSVHandle) error { return mgr.openFileNormal(handle) } -// openFileNormal 正常打开文件 +// 正常打开文件 func (mgr *CSVManager) openFileNormal(handle *CSVHandle) error { file, err := os.Open(handle.Filename) if err != nil { @@ -251,7 +252,7 @@ func (mgr *CSVManager) openFileNormal(handle *CSVHandle) error { return nil } -// openFileWithMMap 使用内存映射打开大文件 +// 使用内存映射打开大文件 func (mgr *CSVManager) openFileWithMMap(handle *CSVHandle, fileSize int64) error { file, err := os.Open(handle.Filename) if err != nil { @@ -271,7 +272,7 @@ func (mgr *CSVManager) openFileWithMMap(handle *CSVHandle, fileSize int64) error return nil } -// readHeader 读取CSV表头 +// 读取CSV表头 func (mgr *CSVManager) readHeader(handle *CSVHandle) error { handle.mu.Lock() defer handle.mu.Unlock() @@ -299,8 +300,8 @@ func (mgr *CSVManager) readHeader(handle *CSVHandle) error { return nil } -// GetHandle 获取句柄对象 -func (mgr *CSVManager) GetHandle(handleID int64) (*CSVHandle, error) { +// 获取句柄对象 +func (mgr *CSVManager) getHandle(handleID int64) (*CSVHandle, error) { value, ok := mgr.handles.Load(handleID) if !ok { return nil, fmt.Errorf("句柄不存在: %d", handleID) @@ -326,10 +327,114 @@ func (mgr *CSVManager) GetHandle(handleID int64) (*CSVHandle, error) { return handle, nil } -// ========================== 读取操作 ========================== +// 启动自动关闭计时器 +func (h *CSVHandle) startAutoClose(timeout time.Duration, mgr *CSVManager) { + h.autoCloseTimer = time.AfterFunc(timeout, func() { + h.mu.Lock() + defer h.mu.Unlock() -// ReadRow 读取一行数据 -func (h *CSVHandle) ReadRow() ([]string, error) { + // 检查是否长时间未访问 + if h.IsOpen && time.Since(h.AccessTime) > timeout { + h.close() + // 从管理器移除 + mgr.handles.Delete(h.ID) + } + }) +} + +// 获取表头 +func (h *CSVHandle) getHeader() []string { + h.mu.RLock() + defer h.mu.RUnlock() + return h.Header +} + +//// 获取句柄信息 +//func (h *CSVHandle) getInfo() map[string]interface{} { +// h.mu.RLock() +// defer h.mu.RUnlock() +// +// return map[string]interface{}{ +// "id": h.ID, +// "filename": h.Filename, +// "delimiter": string(h.Delimiter), +// "has_header": h.HasHeader, +// "is_open": h.IsOpen, +// "open_time": h.OpenTime, +// "access_time": h.AccessTime, +// "access_count": h.AccessCount, +// "total_rows": h.TotalRows, +// "header": h.Header, +// } +//} + +// ========================== 句柄管理 ========================== + +// 关闭CSV句柄 +func (h *CSVHandle) close() error { + h.mu.Lock() + defer h.mu.Unlock() + + h.writeMu.Lock() + defer h.writeMu.Unlock() + + if !h.IsOpen { + return nil + } + + // 停止自动关闭计时器 + if h.autoCloseTimer != nil { + h.autoCloseTimer.Stop() + h.autoCloseTimer = nil + } + + // 关闭文件 + if h.File != nil { + if err := h.File.Close(); err != nil { + return err + } + h.File = nil + } + + h.CSVReader = nil + h.IsOpen = false + + return nil +} + +// 关闭指定句柄 +func (mgr *CSVManager) closeHandle(handleID int64) error { + value, ok := mgr.handles.Load(handleID) + if !ok { + return fmt.Errorf("句柄不存在: %d", handleID) + } + + handle := value.(*CSVHandle) + if err := handle.close(); err != nil { + return err + } + + // 清理文件锁 + mgr.fileLocks.Delete(handle.Filename) + + mgr.handles.Delete(handleID) + return nil +} + +// 关闭所有句柄 +func (mgr *CSVManager) closeAllHandles() { + mgr.handles.Range(func(key, value interface{}) bool { + handle := value.(*CSVHandle) + handle.close() + mgr.fileLocks.Delete(handle.Filename) + mgr.handles.Delete(key) + return true + }) +} + +// ========================== 读取操作 ========================== +// 读取一行数据 +func (h *CSVHandle) readRow() ([]string, error) { h.mu.RLock() defer h.mu.RUnlock() @@ -344,8 +449,8 @@ func (h *CSVHandle) ReadRow() ([]string, error) { return h.CSVReader.Read() } -// ReadAllRows 读取所有行 -func (h *CSVHandle) ReadAllRows() ([][]string, error) { +// 读取所有行 +func (h *CSVHandle) readAllRows() ([][]string, error) { h.mu.Lock() defer h.mu.Unlock() @@ -373,8 +478,8 @@ func (h *CSVHandle) ReadAllRows() ([][]string, error) { return h.CSVReader.ReadAll() } -// ReadRows 读取指定数量的行 -func (h *CSVHandle) ReadRows(count int) ([][]string, error) { +// 读取指定数量的行 +func (h *CSVHandle) readRows(count int) ([][]string, error) { h.mu.Lock() defer h.mu.Unlock() @@ -398,8 +503,8 @@ func (h *CSVHandle) ReadRows(count int) ([][]string, error) { return rows, nil } -// GetTotalRows 获取总行数 -func (h *CSVHandle) GetTotalRows() (int64, error) { +// 获取总行数 +func (h *CSVHandle) getTotalRows() (int64, error) { h.mu.Lock() defer h.mu.Unlock() @@ -450,8 +555,53 @@ func (h *CSVHandle) GetTotalRows() (int64, error) { // ========================== 修改操作 ========================== -// ModifyRow 修改指定行 -func (mgr *CSVManager) ModifyRow(handleID int64, rowNumber int64, newRow []string) (*ModifyResult, error) { +// 写入和覆盖CSV文件 +func (mgr *CSVManager) writeCSVFile(handleID int64, filename string, data [][]string) (int64, error) { + // 1. 获取句柄 + handle, err := mgr.getHandle(handleID) + if err != nil { + return -1, err + } + + if len(data) == 0 { + return -1, fmt.Errorf("写入的数据不能为空") + } + + fileLock := mgr.getFileLock(handle.Filename) + fileLock.Lock() + defer fileLock.Unlock() + + var file *os.File + var fileMode int + + // 检查文件是否存在和有内容 + if info, err := os.Stat(handle.Filename); err == nil && info.Size() > 0 { + // 文件存在且有内容,追加模式 + fileMode = os.O_APPEND | os.O_WRONLY + } else { + // 文件不存在或为空,创建模式(会清空已有内容) + fileMode = os.O_CREATE | os.O_WRONLY | os.O_TRUNC + } + + // 打开或创建文件 + file, err = os.OpenFile(filename, fileMode, 0755) + if err != nil { + return -1, err + } + defer file.Close() + + // 写入CSV数据 + writer := csv.NewWriter(file) + if err := writer.WriteAll(data); err != nil { + return -1, err + } + writer.Flush() + + return -1, writer.Error() +} + +// 修改指定行 +func (mgr *CSVManager) modifyRow(handleID int64, rowNumber int64, newRow []string) (*ModifyResult, error) { req := &ModifyRequest{ HandleID: handleID, RowNumber: rowNumber, @@ -459,26 +609,13 @@ func (mgr *CSVManager) ModifyRow(handleID int64, rowNumber int64, newRow []strin NewRow: newRow, } - return mgr.ModifyCSV(req) + return mgr.modifyCSV(req) } -// ModifyCell 修改指定单元格 -func (mgr *CSVManager) ModifyCell(handleID int64, rowNumber int64, columnIndex int, newValue string) (*ModifyResult, error) { - req := &ModifyRequest{ - HandleID: handleID, - RowNumber: rowNumber, - ModifyType: ModifyCell, - ColumnIndex: columnIndex, - NewCellValue: newValue, - } - - return mgr.ModifyCSV(req) -} - -// ModifyCSV 通用的CSV修改函数 -func (mgr *CSVManager) ModifyCSV(req *ModifyRequest) (*ModifyResult, error) { +// 通用的CSV修改函数 +func (mgr *CSVManager) modifyCSV(req *ModifyRequest) (*ModifyResult, error) { // 1. 获取句柄 - handle, err := mgr.GetHandle(req.HandleID) + handle, err := mgr.getHandle(req.HandleID) if err != nil { return &ModifyResult{ Success: false, @@ -507,7 +644,7 @@ func (mgr *CSVManager) ModifyCSV(req *ModifyRequest) (*ModifyResult, error) { defer handle.writeMu.Unlock() // 6. 执行修改操作 - result, err := mgr.executeModifyWithRetry(handle, req, operationID) + result, err := mgr.executeModifyOnce(handle, req, operationID) if result != nil { result.OperationID = operationID } @@ -533,13 +670,6 @@ func (mgr *CSVManager) validateModifyRequestBeforeLock(handle *CSVHandle, req *M } } - if req.ModifyType == ModifyCell { - expectedCols := len(header) - if req.ColumnIndex < 0 || req.ColumnIndex >= expectedCols { - return fmt.Errorf("列索引超出范围,有效范围: 0-%d", expectedCols-1) - } - } - return nil } @@ -592,15 +722,6 @@ func (mgr *CSVManager) executeModifyOnce(handle *CSVHandle, req *ModifyRequest, }, err } - //// 2. 获取总行数用于验证 - //totalRows, err := mgr.calculateTotalRowsWithLock(handle) - //if err != nil { - // return &ModifyResult{ - // Success: false, - // Message: fmt.Sprintf("获取总行数失败: %v", err), - // }, err - //} - // 3. 验证行号范围 if err := mgr.validateRowNumber(handle, req, totalRows); err != nil { return &ModifyResult{ @@ -800,6 +921,7 @@ func (mgr *CSVManager) createBackup(handle *CSVHandle) (string, error) { } handle.tempDir = tempDir } + fmt.Println("handle.tempDir", handle.tempDir) // 生成备份文件名 backupFile := filepath.Join(handle.tempDir, @@ -1305,7 +1427,7 @@ func (mgr *CSVManager) restoreFromBackup(handle *CSVHandle) error { // GetRow 获取指定行数据 func (mgr *CSVManager) GetRow(handleID int64, rowNumber int64) ([]string, error) { - handle, err := mgr.GetHandle(handleID) + handle, err := mgr.getHandle(handleID) if err != nil { return nil, err } @@ -1320,7 +1442,7 @@ func (mgr *CSVManager) GetRow(handleID int64, rowNumber int64) ([]string, error) // UndoLastModify 撤销最后一次修改 func (mgr *CSVManager) UndoLastModify(handleID int64) (*ModifyResult, error) { - handle, err := mgr.GetHandle(handleID) + handle, err := mgr.getHandle(handleID) if err != nil { return nil, err } @@ -1383,7 +1505,7 @@ func (mgr *CSVManager) UndoLastModify(handleID int64) (*ModifyResult, error) { // CleanupBackups 清理备份文件 func (mgr *CSVManager) CleanupBackups(handleID int64) error { - handle, err := mgr.GetHandle(handleID) + handle, err := mgr.getHandle(handleID) if err != nil { return err } @@ -1406,156 +1528,9 @@ func (mgr *CSVManager) CleanupBackups(handleID int64) error { return nil } -// ========================== 句柄管理 ========================== - -// Close 关闭CSV句柄 -func (h *CSVHandle) Close() error { - h.mu.Lock() - defer h.mu.Unlock() - - h.writeMu.Lock() - defer h.writeMu.Unlock() - - if !h.IsOpen { - return nil - } - - // 停止自动关闭计时器 - if h.autoCloseTimer != nil { - h.autoCloseTimer.Stop() - h.autoCloseTimer = nil - } - - // 关闭文件 - if h.File != nil { - if err := h.File.Close(); err != nil { - return err - } - h.File = nil - } - - h.CSVReader = nil - h.IsOpen = false - - return nil -} - -// startAutoClose 启动自动关闭计时器 -func (h *CSVHandle) startAutoClose(timeout time.Duration, mgr *CSVManager) { - h.autoCloseTimer = time.AfterFunc(timeout, func() { - h.mu.Lock() - defer h.mu.Unlock() - - // 检查是否长时间未访问 - if h.IsOpen && time.Since(h.AccessTime) > timeout { - h.Close() - // 从管理器移除 - mgr.handles.Delete(h.ID) - } - }) -} - -// GetHeader 获取表头 -func (h *CSVHandle) GetHeader() []string { - h.mu.RLock() - defer h.mu.RUnlock() - return h.Header -} - -// GetInfo 获取句柄信息 -func (h *CSVHandle) GetInfo() map[string]interface{} { - h.mu.RLock() - defer h.mu.RUnlock() - - return map[string]interface{}{ - "id": h.ID, - "filename": h.Filename, - "delimiter": string(h.Delimiter), - "has_header": h.HasHeader, - "is_open": h.IsOpen, - "open_time": h.OpenTime, - "access_time": h.AccessTime, - "access_count": h.AccessCount, - "total_rows": h.TotalRows, - "header": h.Header, - } -} - -// CloseHandle 关闭指定句柄 -func (mgr *CSVManager) CloseHandle(handleID int64) error { - value, ok := mgr.handles.Load(handleID) - if !ok { - return fmt.Errorf("句柄不存在: %d", handleID) - } - - handle := value.(*CSVHandle) - if err := handle.Close(); err != nil { - return err - } - - // 清理文件锁 - mgr.fileLocks.Delete(handle.Filename) - - mgr.handles.Delete(handleID) - return nil -} - -// CloseAllHandles 关闭所有句柄 -func (mgr *CSVManager) CloseAllHandles() { - mgr.handles.Range(func(key, value interface{}) bool { - handle := value.(*CSVHandle) - handle.Close() - mgr.fileLocks.Delete(handle.Filename) - mgr.handles.Delete(key) - return true - }) -} - -// ========================== 简便函数 ========================== - -//// OpenCSVFile 打开CSV文件 -//func OpenCSVFile(filename string, delimiter rune, hasHeader bool) (int64, error) { -// return GetManager().OpenCSVFile(filename, delimiter, hasHeader, "utf-8") -//} - -// GetCSVHandle 获取CSV句柄 -func GetCSVHandle(handleID int64) (*CSVHandle, error) { - return GetManager().GetHandle(handleID) -} - -// CloseCSVHandle 关闭CSV句柄 -func CloseCSVHandle(handleID int64) error { - return GetManager().CloseHandle(handleID) -} - -// ModifyCSVRow 修改CSV行 -func ModifyCSVRow(handleID int64, rowNumber int64, newRow []string) (*ModifyResult, error) { - return GetManager().ModifyRow(handleID, rowNumber, newRow) -} - -// ModifyCSVCell 修改CSV单元格 -func ModifyCSVCell(handleID int64, rowNumber int64, columnIndex int, newValue string) (*ModifyResult, error) { - return GetManager().ModifyCell(handleID, rowNumber, columnIndex, newValue) -} - -// GetCSVRow 获取CSV行 -func GetCSVRow(handleID int64, rowNumber int64) ([]string, error) { - return GetManager().GetRow(handleID, rowNumber) -} - -// UndoLastCSVModify 撤销最后修改 -func UndoLastCSVModify(handleID int64) (*ModifyResult, error) { - return GetManager().UndoLastModify(handleID) -} - -// CleanupCSVBackups 清理CSV备份 -func CleanupCSVBackups(handleID int64) error { - return GetManager().CleanupBackups(handleID) -} - // UpdateCSVRowSafe 修改csv文件行数据 func (mgr *CSVManager) UpdateCSVRowSafe(handleID int64, rowNum int, newRow []string) error { - getHandle, err := mgr.GetHandle(handleID) + getHandle, err := mgr.getHandle(handleID) if err != nil { return fmt.Errorf("获取句柄信息失败: %v", err) } @@ -1764,44 +1739,52 @@ func UpdateCSVRowSafe(handleID C.longlong, rowNum C.int, newRow *C.char) *C.char // 主函数 func main() { - //fmt.Println("=== CSV句柄管理器测试 ===") - // - //filename := "csv/taskLog.csv" - //fmt.Printf("1. 创建测试文件: %s\n", filename) - // - //// 2. 打开CSV文件 - //handleID, err := OpenCSVFile(filename, ',', true) - //if err != nil { - // fmt.Printf("打开文件失败: %v\n", err) - // return - //} - //defer CloseCSVHandle(handleID) - // + fmt.Println("=== CSV句柄管理器测试 ===") + + filename := "csv/taskLog1.csv" + fmt.Printf("1. 创建测试文件: %s\n", filename) + + // 2. 打开CSV文件 + handleID, err := GetManager().OpenCSVFile(filename, ',', true) + if err != nil { + fmt.Printf("打开文件失败: %v\n", err) + return + } + defer GetManager().closeHandle(handleID) + + newStr := []string{"9787115524539", "20.00", "2", "上传成功", ""} + + row, err := GetManager().modifyRow(handleID, 3, newStr) + if err != nil { + fmt.Println(err) + } + fmt.Println(row) + //fmt.Printf("2. 打开文件成功,句柄ID: %d\n", handleID) // - ////// 3. 获取句柄信息 - ////handle, err := GetCSVHandle(handleID) - ////if err != nil { - //// fmt.Printf("获取句柄失败: %v\n", err) - //// return - ////} - //// - ////info := handle.GetInfo() - ////fmt.Printf("3. 文件信息:\n") - ////fmt.Printf(" - 文件名: %s\n", info["filename"]) - ////fmt.Printf(" - 总行数: %v\n", info["total_rows"]) - ////fmt.Printf(" - 表头: %v\n", info["header"]) + //// 3. 获取句柄信息 + //handle, err := GetCSVHandle(handleID) + //if err != nil { + // fmt.Printf("获取句柄失败: %v\n", err) + // return + //} // - ////// 准备新的行数据 - ////newData := []string{"9787115524539", "20.00", "20", "上传成功", ""} - ////err = UpdateCSVRowSafes(handleID, 6, newData) - ////if err != nil { - //// fmt.Printf("错误: %v\n", err) - ////} else { - //// fmt.Println("CSV文件更新成功!") - ////} + //info := handle.GetInfo() + //fmt.Printf("3. 文件信息:\n") + //fmt.Printf(" - 文件名: %s\n", info["filename"]) + //fmt.Printf(" - 总行数: %v\n", info["total_rows"]) + //fmt.Printf(" - 表头: %v\n", info["header"]) // - //// 4. 读取数据 + //// 准备新的行数据 + //newData := []string{"9787115524539", "20.00", "20", "上传成功", ""} + //err = UpdateCSVRowSafes(handleID, 6, newData) + //if err != nil { + // fmt.Printf("错误: %v\n", err) + //} else { + // fmt.Println("CSV文件更新成功!") + //} + // + ////4. 读取数据 //fmt.Println("4. 读取测试:") //row3, err := GetCSVRow(handleID, 100000) //if err != nil { @@ -1810,72 +1793,36 @@ func main() { // fmt.Printf(" 第100000行数据: %v\n", row3) //} // - ////// 5. 修改测试 - ////fmt.Println("5. 修改测试:") - ////newRow := []string{"9787115524539", "20.00", "1", "上传成功", ""} - ////result, err := ModifyCSVRow(handleID, 3, newRow) - ////if err != nil { - //// fmt.Printf(" 修改失败: %v\n", err) - ////} else { - //// fmt.Printf(" 修改结果: %s (影响行数: %d)\n", result.Message, result.RowsAffected) - //// - //// // 验证修改 - //// modifiedRow, _ := GetCSVRow(handleID, 3) - //// fmt.Printf(" 修改后的第3行: %v\n", modifiedRow) - ////} + //// 5. 修改测试 + //fmt.Println("5. 修改测试:") + //newRow := []string{"9787115524539", "20.00", "1", "上传成功", ""} + //result, err := ModifyCSVRow(handleID, 3, newRow) + //if err != nil { + // fmt.Printf(" 修改失败: %v\n", err) + //} else { + // fmt.Printf(" 修改结果: %s (影响行数: %d)\n", result.Message, result.RowsAffected) // - ////// 6. 单元格修改测试 - ////fmt.Println("6. 单元格修改测试:") - ////cellResult, err := ModifyCSVCell(handleID, 5, 1, "35") - ////if err != nil { - //// fmt.Printf(" 单元格修改失败: %v\n", err) - ////} else { - //// fmt.Printf(" 单元格修改结果: %s\n", cellResult.Message) - //// - //// // 验证修改 - //// cellModifiedRow, _ := GetCSVRow(handleID, 5) - //// fmt.Printf(" 修改后的第5行: %v\n", cellModifiedRow) - ////} - //// - ////// 7. 并发测试 - ////fmt.Println("7. 并发测试:") - ////TestConcurrentOperations(handleID) - //// - ////fmt.Println("=== 测试完成 ===") -} - -// 下面是一个更简单的版本,根据你的具体需求可以选择使用 -func WriteCSVSimple(filename string, data [][]string) error { - if len(data) == 0 { - return fmt.Errorf("写入的数据不能为空") - } - - var file *os.File - var err error - var fileMode int - - // 检查文件是否存在和有内容 - if info, err := os.Stat(filename); err == nil && info.Size() > 0 { - // 文件存在且有内容,追加模式 - fileMode = os.O_APPEND | os.O_WRONLY - } else { - // 文件不存在或为空,创建模式(会清空已有内容) - fileMode = os.O_CREATE | os.O_WRONLY | os.O_TRUNC - } - - // 打开或创建文件 - file, err = os.OpenFile(filename, fileMode, 0755) - if err != nil { - return err - } - defer file.Close() - - // 写入CSV数据 - writer := csv.NewWriter(file) - if err := writer.WriteAll(data); err != nil { - return err - } - writer.Flush() - - return writer.Error() + // // 验证修改 + // modifiedRow, _ := GetCSVRow(handleID, 3) + // fmt.Printf(" 修改后的第3行: %v\n", modifiedRow) + //} + // + //// 6. 单元格修改测试 + //fmt.Println("6. 单元格修改测试:") + //cellResult, err := ModifyCSVCell(handleID, 5, 1, "35") + //if err != nil { + // fmt.Printf(" 单元格修改失败: %v\n", err) + //} else { + // fmt.Printf(" 单元格修改结果: %s\n", cellResult.Message) + // + // // 验证修改 + // cellModifiedRow, _ := GetCSVRow(handleID, 5) + // fmt.Printf(" 修改后的第5行: %v\n", cellModifiedRow) + //} + // + //// 7. 并发测试 + //fmt.Println("7. 并发测试:") + //TestConcurrentOperations(handleID) + // + //fmt.Println("=== 测试完成 ===") } diff --git a/csv/csvDllTest.go b/csv/csvDllTest.go index 7c85ea0..679de38 100644 --- a/csv/csvDllTest.go +++ b/csv/csvDllTest.go @@ -320,58 +320,6 @@ func handleOpenCSVFile(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } -func main() { - dll, err := InitCsvDLL() - if err != nil { - - } - file, err := dll.CreateOpenCSVFile("csv/taskLog.csv", ',', true) - if err != nil { - - } - newRow := []string{"9787115524539", "100.00", "1", "上传成功", ""} - safe, err := dll.UpdateCSVRowSafe(file.Data.HandleID, 9, newRow) - if err != nil { - - } - marshal, _ := json.Marshal(safe) - fmt.Println(string(marshal)) - - //http.HandleFunc("/csv/openCSVFile", handleOpenCSVFile) - //port := "8080" - //server := &http.Server{ - // Addr: ":" + port, - // Handler: nil, - //} - // - //// 4. 优雅关闭设置 - //done := make(chan bool, 1) - //quit := make(chan os.Signal, 1) - //signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - // - //// 5. 优雅关闭协程 - //go func() { - // <-quit - // fmt.Println("\n服务器正在关闭...") - // - // ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - // defer cancel() - // - // if err := server.Shutdown(ctx); err != nil { - // fmt.Printf("强制关闭服务器: %v\n", err) - // } - // close(done) - //}() - // - //// 启动服务器 - //if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - // fmt.Printf("服务器启动失败: %s\n", err) - //} - //// 7. 等待关闭完成 - //<-done - //fmt.Println("服务器已关闭") -} - // 解码从DLL读取的数据 func decodeRowData(buffer []byte, maxBytes int) [][]string { var rows [][]string @@ -411,3 +359,91 @@ func decodeRowData(buffer []byte, maxBytes int) [][]string { return rows } + +//func main() { +// 使用dll文件 +//dll, err := InitCsvDLL() +//if err != nil { +// +//} +//file, err := dll.CreateOpenCSVFile("csv/taskLog.csv", ',', true) +//if err != nil { +// +//} +//newRow := []string{"9787115524539", "100.00", "1", "上传成功", ""} +//safe, err := dll.UpdateCSVRowSafe(file.Data.HandleID, 9, newRow) +//if err != nil { +// +//} +//marshal, _ := json.Marshal(safe) +//fmt.Println(string(marshal)) + +//file, err := GetManager().OpenCSVFile("csv/taskLog1.csv", ',', true) +//if err != nil { +// fmt.Println(err) +//} +//fmt.Println(file) + +//handle, err := GetManager().getHandle(2) +//if err != nil { +// fmt.Println(err) +//} +// 获取指定数量的行 +//row, err := handle.readRows(100) +//if err != nil { +// fmt.Println(err) +//} +//for _, i := range row { +// fmt.Println(i) +//} + +//// 获取总行数 +//row, err := handle.getTotalRows() +//if err != nil { +// fmt.Println(err) +//} +//fmt.Println(row) +//newRow := []string{ +// "9787107267505", "20.00", "10", "上传成功", "877133619369", +//} +// +//row, err := GetManager().modifyRow(file, 1, newRow) +//if err != nil { +// fmt.Println(err) +//} +//fmt.Println(row) + +//http.HandleFunc("/csv/openCSVFile", handleOpenCSVFile) +//port := "8080" +//server := &http.Server{ +// Addr: ":" + port, +// Handler: nil, +//} +// +//// 4. 优雅关闭设置 +//done := make(chan bool, 1) +//quit := make(chan os.Signal, 1) +//signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) +// +//// 5. 优雅关闭协程 +//go func() { +// <-quit +// fmt.Println("\n服务器正在关闭...") +// +// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +// defer cancel() +// +// if err := server.Shutdown(ctx); err != nil { +// fmt.Printf("强制关闭服务器: %v\n", err) +// } +// close(done) +//}() +// +//// 启动服务器 +//if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { +// fmt.Printf("服务器启动失败: %s\n", err) +//} +//// 7. 等待关闭完成 +//<-done +//fmt.Println("服务器已关闭") +//} diff --git a/csv/csvTool.go b/csv/csvTool.go deleted file mode 100644 index 4bb7e6d..0000000 --- a/csv/csvTool.go +++ /dev/null @@ -1,1050 +0,0 @@ -package main - -///* -//#include -//*/ -//import "C" -//import ( -// "encoding/csv" -// "encoding/json" -// "fmt" -// "io" -// "os" -// "strings" -// "sync" -// "sync/atomic" -// "time" -// "unsafe" -//) -// -//// CSVManager CSV全局管理器 -//type CSVManager struct { -// files sync.Map // handle -> *CSVFile (并发安全的映射) -// nextHandle int64 // 生成唯一句柄 -// errorMsg string // 全局错误信息 -// mu sync.RWMutex // 管理器级别的锁 -//} -// -//// CSVFile 文件结构 -//type CSVFile struct { -// filename string // 实际文件路径 -// handle int64 // 唯一句柄 -// delimiter rune // 分隔符(如 ',') -// hasHeader bool // 是否有标题行 -// -// // 内存数据缓存 -// data [][]string // 内存中的数据行 -// header []string // 标题行(如果有) -// fileSize int64 // 文件大小 -// modified bool // 标记是否被修改 -// -// // 并发控制 -// mu sync.RWMutex // 文件级锁 -// rowLocks []*sync.RWMutex // 行级锁 -// rowMu sync.Mutex // 行锁数组保护 -// readers int32 // 活跃读取器计数 -// writers int32 // 活跃写入器计数 -// saving int32 // 正在保存的goroutine计数 -// saveErr chan error // 保存错误通道 -// done chan struct{} // 关闭信号 -//} -// -//// CSV响应结构体 -//type CSVResponse struct { -// Success bool `json:"success"` -// Message string `json:"message,omitempty"` -// Data interface{} `json:"data,omitempty"` -//} -// -//// 单例模式的管理器 -//var manager *CSVManager -//var once sync.Once -// -//// newCSVFile 创建新的CSVFile对象 -//func newCSVFile(filename string, delimiter rune, hasHeader bool) *CSVFile { -// return &CSVFile{ -// filename: filename, -// delimiter: delimiter, -// hasHeader: hasHeader, -// data: make([][]string, 0), -// rowLocks: make([]*sync.RWMutex, 0), -// saveErr: make(chan error, 1), -// done: make(chan struct{}), -// } -//} -// -//// getManager 获取全局管理器(单例) -//func getManager() *CSVManager { -// once.Do(func() { -// manager = &CSVManager{ -// nextHandle: 1, -// } -// }) -// return manager -//} -// -//// setError 设置错误信息 -//func setError(err string) { -// mgr := getManager() -// mgr.mu.Lock() -// mgr.errorMsg = err -// mgr.mu.Unlock() -//} -// -//// 初始化管理器 -//func initCSVManager() int64 { -// _ = getManager() -// return 0 // 成功 -//} -// -//// 加载CSV文件到内存 -//func (f *CSVFile) load() error { -// f.mu.Lock() -// defer f.mu.Unlock() -// -// // 打开文件 -// file, err := os.Open(f.filename) -// if err != nil { -// // 文件不存在则创建空文件 -// if os.IsNotExist(err) { -// f.data = make([][]string, 0) // 初始化空数据 -// f.rowLocks = make([]*sync.RWMutex, 0) // 初始化空行锁数组 -// return nil -// } -// return fmt.Errorf("文件不存在: %v", err) -// } -// // 确保函数结束时关闭文件 -// defer file.Close() -// -// // 获取文件大小 -// stat, _ := file.Stat() // 获取文件信息 -// f.fileSize = stat.Size() // 获取文件大小 -// -// // 创建CSV读取器 -// reader := csv.NewReader(file) -// reader.Comma = f.delimiter // 设置分隔符(默认逗号) -// reader.LazyQuotes = true // 允许宽松的引号解析 -// reader.TrimLeadingSpace = true // 去除字段前的空格 -// -// // 关键修改:允许变长记录,不强制检查字段数量 -// reader.FieldsPerRecord = -1 -// firstRecord, err := reader.Read() -// if err != nil { -// if err == io.EOF { -// // 空文件 -// f.data = make([][]string, 0) -// f.rowLocks = make([]*sync.RWMutex, 0) -// return nil -// } -// return err -// } -// maxColumns := len(firstRecord) -// allData := [][]string{firstRecord} -// -// // 读取剩余行 -// for { -// record, err := reader.Read() -// if err == io.EOF { -// break -// } -// if err != nil { -// // 对于有问题的行,可以填充或跳过 -// continue -// } -// -// // 确保所有行都有相同的列数 -// if len(record) < maxColumns { -// // 填充缺失的列为空字符串 -// paddedRecord := make([]string, maxColumns) -// copy(paddedRecord, record) -// for i := len(record); i < maxColumns; i++ { -// paddedRecord[i] = "" -// } -// record = paddedRecord -// } else if len(record) > maxColumns { -// // 如果行有更多列,更新最大列数 -// maxColumns = len(record) -// // 重新处理之前的所有行 -// for i := range allData { -// if len(allData[i]) < maxColumns { -// paddedRecord := make([]string, maxColumns) -// copy(paddedRecord, allData[i]) -// allData[i] = paddedRecord -// } -// } -// } -// -// allData = append(allData, record) -// } -// -// if len(allData) == 0 { -// f.data = make([][]string, 0) -// f.rowLocks = make([]*sync.RWMutex, 0) -// return nil -// } -// -// // 处理表头 -// if f.hasHeader && len(allData) > 0 { -// f.header = allData[0] -// f.data = allData[1:] -// } else { -// f.data = allData -// } -// -// // 初始化行锁 -// f.initRowLocks() -// -// return nil -//} -// -//// initRowLocks 初始化行锁数组 -//func (f *CSVFile) initRowLocks() { -// count := len(f.data) -// f.rowLocks = make([]*sync.RWMutex, count) -// for i := 0; i < count; i++ { -// f.rowLocks[i] = &sync.RWMutex{} -// } -//} -// -//// 创建CSV文件到内存(句柄) -//func openCSVFile(filename string, delimiter rune, hasHeader bool) (int64, error) { -// // 创建CSV全局管理器 -// mgr := getManager() -// -// // 生成句柄 -// handle := atomic.AddInt64(&mgr.nextHandle, 1) -// -// // 创建文件对象 -// file := newCSVFile(filename, delimiter, hasHeader) -// file.handle = handle -// -// // 加载文件数据 -// if err := file.load(); err != nil { -// return -1, err -// } -// -// // 存储到管理器 -// mgr.files.Store(handle, file) -// -// return handle, nil -//} - -//// 写入/覆盖行数据 -//func writeRows(handle int64, rowData [][]string, header int) int64 { -// mgr := getManager() -// -// // 获取文件对象 -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("文件无效句柄!") -// return -1 -// } -// -// file := val.(*CSVFile) -// -// // 获取写入锁 -// file.mu.Lock() -// defer file.mu.Unlock() -// -// atomic.AddInt32(&file.writers, 1) -// defer atomic.AddInt32(&file.writers, -1) -// -// // 清空现有数据(因为这是覆盖写入) -// file.data = make([][]string, 0, len(rowData)) -// if header == 0 { -// file.header = rowData[0] -// // 添加新数据 -// file.data = rowData[1:] -// } else { -// // 添加新数据 -// for _, row := range rowData { -// file.data = append(file.data, row) -// } -// } -// -// // 扩展行锁数组 -// file.rowMu.Lock() -// file.rowLocks = make([]*sync.RWMutex, len(file.data)) -// for i := range file.rowLocks { -// file.rowLocks[i] = &sync.RWMutex{} -// } -// file.rowMu.Unlock() -// -// file.modified = true -// // 异步保存到文件 -// go file.saveAsync() -// return int64(len(file.data)) -//} -// -//// getRowLock 获取行锁(线程安全) -//func (f *CSVFile) getRowLock(rowIndex int) *sync.RWMutex { -// if rowIndex < 0 { -// return nil -// } -// -// f.rowMu.Lock() -// defer f.rowMu.Unlock() -// -// // 确保行锁存在 -// if rowIndex >= len(f.rowLocks) { -// // 扩展行锁数组 -// newLocks := make([]*sync.RWMutex, rowIndex+1) -// copy(newLocks, f.rowLocks) -// for i := len(f.rowLocks); i <= rowIndex; i++ { -// newLocks[i] = &sync.RWMutex{} -// } -// f.rowLocks = newLocks -// } -// -// return f.rowLocks[rowIndex] -//} -// -//// save 保存到文件 -//func (f *CSVFile) save() error { -// // 标记正在保存 -// if !atomic.CompareAndSwapInt32(&f.saving, 0, 1) { -// // 已经在保存中,直接返回 -// return nil -// } -// defer atomic.StoreInt32(&f.saving, 0) -// -// // 如果没有修改,直接返回 -// f.mu.RLock() -// if !f.modified { -// f.mu.RUnlock() -// return nil -// } -// // 复制数据 -// dataCopy := make([][]string, len(f.data)) -// -// for i := range f.data { -// dataCopy[i] = make([]string, len(f.data[i])) -// copy(dataCopy[i], f.data[i]) -// } -// // 复制表头 -// headerCopy := make([]string, len(f.header)) -// copy(headerCopy, f.header) -// // 复制配置(值类型,直接赋值) -// hasHeader := f.hasHeader -// delimiter := f.delimiter -// filename := f.filename -// f.mu.RUnlock() // 释放读锁 -// -// // 创建临时文件(使用不同的扩展名避免冲突) -// tempFile := filename + ".tmp" -// file, err := os.Create(tempFile) -// if err != nil { -// return err -// } -// -// // 创建CSV写入器 -// writer := csv.NewWriter(file) -// writer.Comma = delimiter -// -// // 写入数据 -// if hasHeader && len(headerCopy) > 0 { -// if err := writer.Write(headerCopy); err != nil { -// file.Close() -// os.Remove(tempFile) -// return err -// } -// } -// -// if err := writer.WriteAll(dataCopy); err != nil { -// file.Close() -// os.Remove(tempFile) -// return err -// } -// -// writer.Flush() -// if err := writer.Error(); err != nil { -// file.Close() -// os.Remove(tempFile) -// return err -// } -// -// // 关闭文件,确保数据写入磁盘 -// if err := file.Close(); err != nil { -// os.Remove(tempFile) -// return err -// } -// -// // 尝试重命名,如果失败可能是文件被占用 -// var renameErr error -// for retry := 0; retry < 3; retry++ { -// renameErr = os.Rename(tempFile, filename) -// if renameErr == nil { -// break -// } -// time.Sleep(100 * time.Millisecond) // 等待重试 -// } -// -// if renameErr != nil { -// os.Remove(tempFile) -// return renameErr -// } -// -// // 标记为已保存 -// f.mu.Lock() -// f.modified = false -// f.mu.Unlock() -// -// return nil -//} -// -//// saveAsync 异步保存,带错误处理 -//func (f *CSVFile) saveAsync() { -// select { -// case f.saveErr <- f.save(): -// // 保存完成 -// default: -// // 通道已满,忽略错误 -// } -//} -// -//// 读取多行数据 -//func readRows(handle int64, startRow, count int64, buffer []byte) int64 { -// mgr := getManager() -// -// // 获取文件对象 -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("Invalid file handle") -// return -1 -// } -// -// file := val.(*CSVFile) -// atomic.AddInt32(&file.readers, 1) -// defer atomic.AddInt32(&file.readers, -1) -// -// // 获取读取锁 -// file.mu.RLock() -// defer file.mu.RUnlock() -// -// totalRows := int64(len(file.data)) -// if startRow < 0 || startRow >= totalRows { -// return 0 -// } -// -// // 计算实际读取行数 -// endRow := startRow + count -// if endRow > totalRows { -// endRow = totalRows -// } -// -// rowsToRead := endRow - startRow -// -// // 将数据复制到缓冲区 -// bytesWritten := 0 -// -// for i := startRow; i < endRow; i++ { -// row := file.data[i] -// -// // 获取行读锁 -// if rowLock := file.getRowLock(int(i)); rowLock != nil { -// rowLock.RLock() -// } -// -// for _, cell := range row { -// // 写入单元格长度 -// cellLen := len(cell) -// if bytesWritten+4 > len(buffer) { -// break -// } -// -// // 写入4字节长度 -// buffer[bytesWritten] = byte(cellLen & 0xFF) -// buffer[bytesWritten+1] = byte((cellLen >> 8) & 0xFF) -// buffer[bytesWritten+2] = byte((cellLen >> 16) & 0xFF) -// buffer[bytesWritten+3] = byte((cellLen >> 24) & 0xFF) -// bytesWritten += 4 -// -// // 写入单元格数据 -// if bytesWritten+cellLen > len(buffer) { -// // 缓冲区不足,回退长度写入 -// bytesWritten -= 4 -// break -// } -// -// copy(buffer[bytesWritten:bytesWritten+cellLen], cell) -// bytesWritten += cellLen -// } -// -// // 写入行结束标记 -// if bytesWritten+4 <= len(buffer) { -// // 4字节的0表示行结束 -// buffer[bytesWritten] = 0 -// buffer[bytesWritten+1] = 0 -// buffer[bytesWritten+2] = 0 -// buffer[bytesWritten+3] = 0 -// bytesWritten += 4 -// } -// -// // 释放行锁 -// if rowLock := file.getRowLock(int(i)); rowLock != nil { -// rowLock.RUnlock() -// } -// -// if bytesWritten >= len(buffer) { -// break -// } -// } -// -// return rowsToRead -//} -// -//// 追加行数据 -//func appendRows(handle int64, rowsData []byte, rowCount int64) int { -// mgr := getManager() -// -// // 获取文件对象 -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("Invalid file handle") -// return -1 -// } -// -// file := val.(*CSVFile) -// -// // 获取写入锁 -// file.mu.Lock() -// atomic.AddInt32(&file.writers, 1) -// -// // 解析行数据 -// offset := 0 -// -// for i := int64(0); i < rowCount; i++ { -// var row []string -// -// // 读取行数据 -// for { -// if offset+4 > len(rowsData) { -// break -// } -// -// cellLen := int(uint32(rowsData[offset]) | -// uint32(rowsData[offset+1])<<8 | -// uint32(rowsData[offset+2])<<16 | -// uint32(rowsData[offset+3])<<24) -// offset += 4 -// -// if cellLen == 0 { -// break -// } -// -// if offset+cellLen > len(rowsData) { -// break -// } -// -// cell := string(rowsData[offset : offset+cellLen]) -// offset += cellLen -// row = append(row, cell) -// } -// -// // 追加到数据 -// file.data = append(file.data, row) -// } -// -// file.modified = true -// -// atomic.AddInt32(&file.writers, -1) -// file.mu.Unlock() -// -// // 异步保存 -// go file.saveAsync() -// -// return 0 -//} -// -//// 获取总行数 -//func getRowCount(handle int64) int64 { -// mgr := getManager() -// -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("Invalid file handle") -// return -1 -// } -// -// file := val.(*CSVFile) -// file.mu.RLock() -// defer file.mu.RUnlock() -// -// return int64(len(file.data)) -//} -// -//// 搜索行 -//func findRows(handle int64, searchText string, columnIndex int64, resultBuffer []byte, maxResults int64) int64 { -// mgr := getManager() -// -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("Invalid file handle") -// return -1 -// } -// -// file := val.(*CSVFile) -// -// file.mu.RLock() -// defer file.mu.RUnlock() -// -// atomic.AddInt32(&file.readers, 1) -// defer atomic.AddInt32(&file.readers, -1) -// -// var foundRows []int64 -// -// // 搜索行 -// for i, row := range file.data { -// if maxResults > 0 && int64(len(foundRows)) >= maxResults { -// break -// } -// -// // 获取行读锁 -// if rowLock := file.getRowLock(i); rowLock != nil { -// rowLock.RLock() -// } -// -// // 检查列 -// if columnIndex < 0 || columnIndex >= int64(len(row)) { -// // 搜索所有列 -// for _, cell := range row { -// if cell == searchText { -// foundRows = append(foundRows, int64(i)) -// break -// } -// } -// } else if row[columnIndex] == searchText { -// foundRows = append(foundRows, int64(i)) -// } -// -// // 释放行锁 -// if rowLock := file.getRowLock(i); rowLock != nil { -// rowLock.RUnlock() -// } -// } -// -// // 写入结果到缓冲区 -// if resultBuffer != nil && len(foundRows) > 0 { -// bytesWritten := 0 -// -// for _, rowIndex := range foundRows { -// if bytesWritten+8 > len(resultBuffer) { -// break -// } -// -// // 写入行索引(8字节) -// for j := 0; j < 8; j++ { -// resultBuffer[bytesWritten] = byte((rowIndex >> (uint(j) * 8)) & 0xFF) -// bytesWritten++ -// } -// } -// } -// -// return int64(len(foundRows)) -//} -// -//// 合并多个CSV文件(线程安全) -//func mergeCSVFiles(handles []int64, outputFilename string, delimiter rune, hasHeader bool) int64 { -// mgr := getManager() -// -// // 验证所有句柄并获取文件对象 -// files := make([]*CSVFile, 0, len(handles)) -// for _, handle := range handles { -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("Invalid file handle: " + string(handle)) -// return -1 -// } -// files = append(files, val.(*CSVFile)) -// } -// -// // 创建输出文件对象 -// outputFile := newCSVFile(outputFilename, delimiter, hasHeader) -// outputFile.modified = true // 标记为需要保存 -// -// // 第一步:合并表头 -// mergedHeader := make([]string, 0) -// headerSet := make(map[string]bool) -// -// if hasHeader { -// // 收集所有不重复的表头 -// for _, file := range files { -// file.mu.RLock() -// if file.hasHeader && len(file.header) > 0 { -// for _, h := range file.header { -// if !headerSet[h] { -// headerSet[h] = true -// mergedHeader = append(mergedHeader, h) -// } -// } -// } -// file.mu.RUnlock() -// } -// -// // 如果没有找到表头,创建一个默认的表头 -// if len(mergedHeader) == 0 && len(files) > 0 { -// files[0].mu.RLock() -// maxColumns := len(files[0].data[0]) -// files[0].mu.RUnlock() -// -// for i := 0; i < maxColumns; i++ { -// mergedHeader = append(mergedHeader, fmt.Sprintf("Column%d", i+1)) -// } -// } -// } -// outputFile.header = mergedHeader -// -// // 第二步:合并数据 -// mergedData := make([][]string, 0) -// -// // 为每个输入文件创建读取锁并并发读取 -// var wg sync.WaitGroup -// dataChan := make(chan [][]string, len(files)) -// errorChan := make(chan error, len(files)) -// -// for idx, file := range files { -// wg.Add(1) -// go func(fileIdx int, f *CSVFile) { -// defer wg.Done() -// -// // 获取文件的读取锁 -// f.mu.RLock() -// defer f.mu.RUnlock() -// -// // 增加活跃读取器计数 -// atomic.AddInt32(&f.readers, 1) -// defer atomic.AddInt32(&f.readers, -1) -// -// // 读取数据 -// fileData := make([][]string, len(f.data)) -// for i := 0; i < len(f.data); i++ { -// // 获取行读锁 -// if rowLock := f.getRowLock(i); rowLock != nil { -// rowLock.RLock() -// } -// -// // 复制行数据 -// row := make([]string, len(f.data[i])) -// copy(row, f.data[i]) -// -// // 释放行锁 -// if rowLock := f.getRowLock(i); rowLock != nil { -// rowLock.RUnlock() -// } -// -// fileData[i] = row -// } -// -// // 如果需要处理表头映射 -// if hasHeader && f.hasHeader && len(f.header) > 0 { -// // 创建列映射:源列 -> 目标列 -// columnMapping := make(map[int]int) -// for srcIdx, srcHeader := range f.header { -// for dstIdx, dstHeader := range mergedHeader { -// if srcHeader == dstHeader { -// columnMapping[srcIdx] = dstIdx -// break -// } -// } -// } -// -// // 重新排列数据以匹配合并后的表头 -// for i := 0; i < len(fileData); i++ { -// newRow := make([]string, len(mergedHeader)) -// for srcIdx, dstIdx := range columnMapping { -// if srcIdx < len(fileData[i]) { -// newRow[dstIdx] = fileData[i][srcIdx] -// } -// } -// fileData[i] = newRow -// } -// } else if hasHeader && len(mergedHeader) > 0 { -// // 文件没有表头,但输出需要表头 -// // 简单地将数据填充到对应位置 -// for i := 0; i < len(fileData); i++ { -// newRow := make([]string, len(mergedHeader)) -// for j := 0; j < len(fileData[i]) && j < len(newRow); j++ { -// newRow[j] = fileData[i][j] -// } -// fileData[i] = newRow -// } -// } -// -// // 将处理后的数据发送到通道 -// dataChan <- fileData -// }(idx, file) -// } -// -// // 等待所有goroutine完成 -// go func() { -// wg.Wait() -// close(dataChan) -// close(errorChan) -// }() -// -// // 收集所有数据 -// for data := range dataChan { -// mergedData = append(mergedData, data...) -// } -// -// // 检查是否有错误 -// select { -// case err := <-errorChan: -// if err != nil { -// setError("Error merging files: " + err.Error()) -// return -1 -// } -// default: -// } -// -// // 第三步:设置输出文件的数据 -// outputFile.data = mergedData -// outputFile.initRowLocks() -// -// // 第四步:保存到文件 -// if err := outputFile.save(); err != nil { -// setError("Error saving merged file: " + err.Error()) -// return -1 -// } -// -// // 第五步:将输出文件添加到管理器 -// handle := atomic.AddInt64(&mgr.nextHandle, 1) -// outputFile.handle = handle -// mgr.files.Store(handle, outputFile) -// -// return handle -//} -// -//// 关闭文件 -//func closeCSVFile(handle int64) int64 { -// mgr := getManager() -// -// val, ok := mgr.files.Load(handle) -// if !ok { -// setError("文件句柄无效!") -// return -1 -// } -// -// file := val.(*CSVFile) -// -// // 等待所有读写操作完成 -// for atomic.LoadInt32(&file.readers) > 0 || atomic.LoadInt32(&file.writers) > 0 { -// time.Sleep(time.Millisecond) -// } -// -// // 等待异步保存完成 -// for i := 0; i < 100; i++ { // 最多等待100ms -// if atomic.LoadInt32(&file.saving) == 0 { -// break -// } -// time.Sleep(time.Millisecond) -// } -// -// // 如果有正在进行的保存,等待一小段时间 -// if atomic.LoadInt32(&file.saving) > 0 { -// time.Sleep(50 * time.Millisecond) -// } -// -// // 检查是否需要保存 -// file.mu.RLock() -// needSave := file.modified -// file.mu.RUnlock() -// -// if needSave { -// // 同步保存修改 -// if err := file.save(); err != nil { -// setError(err.Error()) -// return -1 -// } -// } -// -// // 从管理器移除 -// mgr.files.Delete(handle) -// -// // 安全关闭通道(如果存在) -// if file.done != nil { -// close(file.done) -// } -// -// return 0 -//} -// -//// 获取错误信息 -//func getError() string { -// mgr := getManager() -// mgr.mu.RLock() -// defer mgr.mu.RUnlock() -// -// if mgr.errorMsg == "" { -// return "" -// } -// -// err := mgr.errorMsg -// mgr.errorMsg = "" // 清空错误 -// -// return err -//} -// -//// ============ C 导出接口 ============ -// -////export InitCSVManager -//func InitCSVManager() C.longlong { -// return C.longlong(initCSVManager()) -//} -// -//// OpenCSVFile 创建CSV文件到内存 -//// -////export OpenCSVFile -//func OpenCSVFile(filename *C.char, delimiter C.char, hasHeader C.int) *C.char { -// goFilename := C.GoString(filename) -// goDelimiter := rune(delimiter) -// var hasHeaderBool bool -// if hasHeader != 0 { -// hasHeaderBool = true -// } -// hasHeaderBool = false -// handle, err := openCSVFile(goFilename, goDelimiter, hasHeaderBool) -// response := struct { -// Handle int64 `json:"handle"` -// }{ -// Handle: handle, -// } -// var csvResponse CSVResponse -// if err != nil { -// csvResponse = CSVResponse{ -// Success: false, -// Message: err.Error(), -// } -// } else { -// csvResponse = CSVResponse{ -// Success: false, -// Data: response, -// } -// } -// // 转换为JSON字符串 -// jsonData, err := json.Marshal(csvResponse) -// if err != nil { -// // 如果JSON序列化失败,返回错误信息 -// csvResponse = CSVResponse{ -// Success: false, -// Message: fmt.Sprintf("JSON序列化失败: %v", err), -// } -// errorJson, _ := json.Marshal(csvResponse) -// return C.CString(string(errorJson)) -// } -// return C.CString(string(jsonData)) -//} -// -//// ReadRows 读取多行数据 -//// -////export ReadRows -//func ReadRows(handle C.longlong, startRow C.longlong, count C.longlong, buffer *C.char, bufferSize C.int) C.longlong { -// // 将 C 缓冲区转换为 Go 的字节切片 -// goBuffer := unsafe.Slice((*byte)(unsafe.Pointer(buffer)), int(bufferSize)) -// result := readRows(int64(handle), int64(startRow), int64(count), goBuffer) -// return C.longlong(result) -//} -// -//// WriteRows 写入/覆盖行数据 -//// -////export WriteRows -//func WriteRows(handle C.longlong, rowsData *C.char, header C.int) C.int { -// goData := C.GoString(rowsData) -// goHeader := int(header) -// data := parseSimpleTable(goData) -// result := writeRows(int64(handle), data, goHeader) -// return C.int(result) -//} -// -//func parseSimpleTable(goData string) [][]string { -// lines := strings.Split(strings.TrimSpace(goData), "\n") -// result := make([][]string, len(lines)) -// -// for i, line := range lines { -// // 根据你的分隔符分割,这里用逗号举例 -// fields := strings.Split(line, ",") -// // 如果需要去除每个字段的空格 -// for j := range fields { -// fields[j] = strings.TrimSpace(fields[j]) -// } -// result[i] = fields -// } -// -// return result -//} -// -//// AppendRows 追加行数据 -//// -////export AppendRows -//func AppendRows(handle C.longlong, rowsData *C.char, dataSize C.int, rowCount C.longlong) C.int { -// goData := unsafe.Slice((*byte)(unsafe.Pointer(rowsData)), int(dataSize)) -// result := appendRows(int64(handle), goData, int64(rowCount)) -// return C.int(result) -//} -// -//// GetRowCount 获取总行数 -//// -////export GetRowCount -//func GetRowCount(handle C.longlong) C.longlong { -// result := getRowCount(int64(handle)) -// return C.longlong(result) -//} -// -//// FindRows 搜索行 -//// -////export FindRows -//func FindRows(handle C.longlong, searchText *C.char, columnIndex C.longlong, resultBuffer *C.char, bufferSize C.int, maxResults C.longlong) C.longlong { -// goSearchText := C.GoString(searchText) -// goResultBuffer := unsafe.Slice((*byte)(unsafe.Pointer(resultBuffer)), int(bufferSize)) -// result := findRows(int64(handle), goSearchText, int64(columnIndex), goResultBuffer, int64(maxResults)) -// return C.longlong(result) -//} -// -//// MergeCSVFiles 合并多个CSV文件(线程安全) -//// -////export MergeCSVFiles -//func MergeCSVFiles(handlesPtr *C.longlong, handlesCount C.int, outputFilename *C.char, delimiter C.char, hasHeader C.int) C.longlong { -// // 将C数组转换为Go切片 -// goHandles := unsafe.Slice(handlesPtr, int(handlesCount)) -// handles := make([]int64, len(goHandles)) -// for i := 0; i < len(goHandles); i++ { -// handles[i] = int64(goHandles[i]) -// } -// // 调用合并函数 -// result := mergeCSVFiles(handles, C.GoString(outputFilename), rune(delimiter), hasHeader != 0) -// return C.longlong(result) -//} -// -//// CloseCSVFile 关闭文件 -//// -////export CloseCSVFile -//func CloseCSVFile(handle C.longlong) C.int { -// result := closeCSVFile(int64(handle)) -// return C.int(result) -//} -// -//// GetError 获取错误信息 -//// -////export GetError -//func GetError(buffer *C.char, bufferSize C.int) C.int { -// errMsg := getError() -// if errMsg == "" { -// return 0 -// } -// -// // 将错误信息复制到缓冲区 -// goBuffer := unsafe.Slice((*byte)(unsafe.Pointer(buffer)), int(bufferSize)) -// n := copy(goBuffer, errMsg) -// return C.int(n) -//} -// -//// 导出函数:释放C字符串内存 -//// -////export FreeCString -//func FreeCString(str *C.char) { -// C.free(unsafe.Pointer(str)) -//} -// -//// main 函数是必需的,即使为空 -////func main() { -////} diff --git a/csv/newcsv.go b/csv/newcsv.go index 1940d46..52dd780 100644 --- a/csv/newcsv.go +++ b/csv/newcsv.go @@ -657,6 +657,7 @@ package main // } // // fmt.Printf("✅ 成功更新第 %d 行,文件已直接更新\n", rowNum) +// // return nil //} // @@ -1220,6 +1221,6 @@ package main // C.free(unsafe.Pointer(str)) //} // -////// main 函数是必需的,即使为空 -////func main() { -////} +//// main 函数是必需的,即使为空 +//func main() { +//} diff --git a/csv/test.go b/csv/test.go deleted file mode 100644 index ce351c7..0000000 --- a/csv/test.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -//func main() { -//filename := filepath.Join("csv", "test1.csv") -//handle := openCSVFile(filename, ',', true) -//fmt.Println("handle:", handle) -// -//// 测试1:基础写入 -//fmt.Println("\n=== 测试1:基础写入 ===") -//// 清空并写入新数据 -//newRow := make([][]string, 0) -//newRow = append(newRow, []string{"基础测试行5", "基础值5", "基础数据5", "基础测试5"}) -// -//for i := 1; i <= 10; i++ { -// row := []string{ -// "基础行" + strconv.Itoa(i), -// "基础值" + strconv.Itoa(i), -// "基础数据" + strconv.Itoa(i), -// fmt.Sprintf("基础测试%d", i), -// } -// newRow = append(newRow, row) -//} -// -//start := time.Now() -//rows := writeRows(handle, newRow, 0) -// -//elapsed := time.Since(start) -//fmt.Printf("基础写入完成,影响行数: %d,耗时: %v\n", rows, elapsed) -// -//buffer := make([]byte, 4096) -//rowss := readRows(handle, 0, rows, buffer) -//fmt.Printf("读取到 %d 行数据\n", rowss) -// -//decodedRows := decodeRowData(buffer, 4096) -//fmt.Println("合并文件前几行数据:") -//for i, row := range decodedRows { -// if i >= int(rowss) { -// break -// } -// fmt.Printf(" 行 %d: %v\n", i, row) -//} - -//// 测试3:读写混合 -//fmt.Println("\n=== 测试3:读写混合 ===") -//testMixedOperations(handle) - -//file, err := closeCSVFile(handle) -//if err != nil { -// fmt.Println(err) -//} -//if file == 0 { -// fmt.Println("\n文件保存成功") -//} -//} diff --git a/es/dll/es.dll b/es/dll/es.dll new file mode 100644 index 0000000..4e41989 Binary files /dev/null and b/es/dll/es.dll differ diff --git a/es/dll/es.h b/es/dll/es.h new file mode 100644 index 0000000..88af26d --- /dev/null +++ b/es/dll/es.h @@ -0,0 +1,141 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package command-line-arguments */ + + +#line 1 "cgo-builtin-export-prolog" + +#include + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { const char *p; ptrdiff_t n; } _GoString_; +extern size_t _GoStringLen(_GoString_ s); +extern const char *_GoStringPtr(_GoString_ s); +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + + +#line 3 "es.go" + +#include + +#line 1 "cgo-generated-wrapper" + + +/* End of preamble from import "C" comments. */ + + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; +typedef unsigned char GoUint8; +typedef short GoInt16; +typedef unsigned short GoUint16; +typedef int GoInt32; +typedef unsigned int GoUint32; +typedef long long GoInt64; +typedef unsigned long long GoUint64; +typedef GoInt64 GoInt; +typedef GoUint64 GoUint; +typedef size_t GoUintptr; +typedef float GoFloat32; +typedef double GoFloat64; +#ifdef _MSC_VER +#if !defined(__cplusplus) || _MSVC_LANG <= 201402L +#include +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +#include +typedef std::complex GoComplex64; +typedef std::complex GoComplex128; +#endif +#else +typedef float _Complex GoComplex64; +typedef double _Complex GoComplex128; +#endif + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; +#endif +typedef void *GoMap; +typedef void *GoChan; +typedef struct { void *t; void *v; } GoInterface; +typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + + +// ListAllIndices 查询所有索引 +// +extern __declspec(dllexport) char* ListAllIndices(void); + +// GetIndicesInfo 获取所有索引的详细信息 +// +extern __declspec(dllexport) char* GetIndicesInfo(void); + +// GetIndexDetail 获取单个索引的详细信息 +// +extern __declspec(dllexport) char* GetIndexDetail(char* indexName); + +// CreateIndex 创建索引(如果不存在) +// +extern __declspec(dllexport) char* CreateIndex(char* indexName, char* mapping); + +// DeleteIndex 删除索引 +// +extern __declspec(dllexport) char* DeleteIndex(char* indexName); + +// GetDocumentCount 获取索引文档数量 +// +extern __declspec(dllexport) char* GetDocumentCount(char* indexName); + +// CreateDocument 创建文档 +// +extern __declspec(dllexport) char* CreateDocument(char* indexName, char* id, char* doc); + +// GetDocument 根据ID获取文档 +// +extern __declspec(dllexport) char* GetDocument(char* indexName, char* id); + +// UpdateDocument 更新文档 +// +extern __declspec(dllexport) char* UpdateDocument(char* indexName, char* id, char* updateData); + +// DeleteDocument 删除文档 +// +extern __declspec(dllexport) char* DeleteDocument(char* indexName, char* id); + +// SearchDocuments 搜索文档 +// +extern __declspec(dllexport) char* SearchDocuments(char* indexName, char* query); + +// 释放C字符串内存 +// +extern __declspec(dllexport) void FreeCString(char* str); + +#ifdef __cplusplus +} +#endif diff --git a/es/es.go b/es/es.go index 06ab7d0..16cc19e 100644 --- a/es/es.go +++ b/es/es.go @@ -1 +1,1089 @@ package main + +/* +#include +*/ +import "C" +import ( + "context" + "encoding/json" + "fmt" + "github.com/elastic/go-elasticsearch/v8" + "github.com/elastic/go-elasticsearch/v8/esapi" + "log" + "strings" + "unsafe" +) + +// ESClient 封装Elasticsearch客户端 +type ESClient struct { + client *elasticsearch.Client +} + +// IndexInfo 索引信息结构 +type IndexInfo struct { + Name string `json:"index"` // 索引名称 + Health string `json:"health"` // 健康状态 green:所有主分片和副本分片都正常 yellow:主分片正常,但部分副本分片不正常 red:有主分片不正常 + Status string `json:"status"` // 索引状态 open:为打开(可用) + UUID string `json:"uuid"` // 索引的唯一标识符 + Pri string `json:"pri"` // 主分片数量 + Rep string `json:"rep"` // 副本分片数量 + DocsCount string `json:"docs.count"` // 文档总数 + DocsDeleted string `json:"docs.deleted"` // 已删除文档数 + StoreSize string `json:"store.size"` // 总存储大小 + PriStoreSize string `json:"pri.store.size"` // 主分片存储大小 + Settings map[string]interface{} `json:"settings,omitempty"` // 索引设置 + Mappings map[string]interface{} `json:"mappings,omitempty"` // 索引映射 + Aliases []string `json:"aliases,omitempty"` // 索引别名 +} + +// ES配置常量 +const ( + esAddress = "http://103.236.91.138:9200" // ES地址 + esUsername = "elastic" // 用户名(如果有认证) + esPassword = "5mRDIUg52VC0fp14nw-F" // 密码(如果有认证) +) + +// newESClient 创建ES客户端 +func newESClient(addresses []string, username, password string) (*ESClient, error) { + cfg := elasticsearch.Config{ + Addresses: addresses, // ES节点地址 + Username: username, // 用户名(如果有认证) + Password: password, // 密码(如果有认证) + } + // 创建新的Elasticsearch客户端 + client, err := elasticsearch.NewClient(cfg) + if err != nil { + return nil, fmt.Errorf("创建ES客户端失败: %w", err) + } + + // 测试连接:发送ping请求验证连接是否正常 + res, err := client.Ping() + if err != nil { + return nil, fmt.Errorf("连接ES失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + return nil, fmt.Errorf("ES响应错误: %s", res.String()) + } + + log.Println("✅ 成功连接到Elasticsearch") + return &ESClient{ + client: client, // 返回封装的客户端 + }, nil +} + +// 查询所有索引 +func (es *ESClient) listAllIndices() ([]string, error) { + // 创建索引列表请求 + req := esapi.CatIndicesRequest{ + Format: "json", // 返回JSON格式 + Pretty: true, // 美化输出 + } + // 执行请求 + res, err := req.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("查询索引列表失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + return nil, fmt.Errorf("查询索引列表失败: %s", res.String()) + } + + // 解析返回的索引信息 + var indices []map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&indices); err != nil { + return nil, fmt.Errorf("解析索引列表失败: %w", err) + } + // 提取索引名称 + var indexNames []string + for _, idx := range indices { + if name, ok := idx["index"].(string); ok && name != "" { + indexNames = append(indexNames, name) + } + } + + return indexNames, nil +} + +// 获取所有索引的详细信息 +func (es *ESClient) getIndicesInfo() ([]IndexInfo, error) { + // 先获取所有索引名 + indexNames, err := es.listAllIndices() + if err != nil { + return nil, err + } + + // 如果有索引,获取详细信息 + if len(indexNames) == 0 { + return []IndexInfo{}, nil + } + + // 获取索引的统计信息 + statsReq := esapi.IndicesStatsRequest{ + Index: indexNames, // 指定要查询的索引 + Human: true, // 返回人类可读的格式 + } + + statsRes, err := statsReq.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("获取索引统计信息失败: %w", err) + } + defer statsRes.Body.Close() // 确保响应体被关闭 + + if statsRes.IsError() { + return nil, fmt.Errorf("获取索引统计信息失败: %s", statsRes.String()) + } + + var statsResult map[string]interface{} + if err := json.NewDecoder(statsRes.Body).Decode(&statsResult); err != nil { + return nil, fmt.Errorf("解析索引统计信息失败: %w", err) + } + + // 获取索引的健康状态 + healthReq := esapi.CatIndicesRequest{ + Index: indexNames, // 指定要查询的索引 + Format: "json", // 返回JSON格式 + H: []string{"index,health,status,pri,rep,docs.count,docs.deleted,store.size,pri.store.size"}, // 要返回的字段 + } + + healthRes, err := healthReq.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("获取索引健康状态失败: %w", err) + } + defer healthRes.Body.Close() // 确保响应体被关闭 + + if healthRes.IsError() { + return nil, fmt.Errorf("获取索引健康状态失败: %s", healthRes.String()) + } + + var healthInfo []map[string]interface{} + if err := json.NewDecoder(healthRes.Body).Decode(&healthInfo); err != nil { + return nil, fmt.Errorf("解析索引健康状态失败: %w", err) + } + + // 组合索引信息 + var indicesInfo []IndexInfo + for _, health := range healthInfo { + indexName, _ := health["index"].(string) + if indexName == "" { + continue // 跳过空索引名 + } + // 构建索引信息结构 + info := IndexInfo{ + Name: indexName, + Health: getString(health, "health", "unknown"), + Status: getString(health, "status", "unknown"), + Pri: getString(health, "pri", "0"), + Rep: getString(health, "rep", "0"), + DocsCount: getString(health, "docs.count", "0"), + DocsDeleted: getString(health, "docs.deleted", "0"), + StoreSize: getString(health, "store.size", "0b"), + PriStoreSize: getString(health, "pri.store.size", "0b"), + } + + indicesInfo = append(indicesInfo, info) + } + + return indicesInfo, nil +} + +// 获取单个索引的详细信息 +func (es *ESClient) getIndexDetail(indexName string) (map[string]interface{}, error) { + req := esapi.IndicesGetRequest{ + Index: []string{indexName}, // 指定要查询的索引 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("获取索引详情失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return nil, fmt.Errorf("索引不存在: %s", indexName) + } + return nil, fmt.Errorf("获取索引详情失败: %s", res.String()) + } + + var result map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("解析索引详情失败: %w", err) + } + + return result, nil +} + +// 获取索引设置 +func (es *ESClient) getIndexSettings(indexName string) (map[string]interface{}, error) { + req := esapi.IndicesGetSettingsRequest{ + Index: []string{indexName}, // 指定要查询的索引 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("获取索引设置失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return nil, fmt.Errorf("索引不存在: %s", indexName) + } + return nil, fmt.Errorf("获取索引设置失败: %s", res.String()) + } + + var result map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("解析索引设置失败: %w", err) + } + + return result, nil +} + +// createIndex 创建索引(如果不存在) +func (es *ESClient) createIndex(indexName string, mapping string) (map[string]interface{}, error) { + // 检查索引是否存在 + res, err := es.client.Indices.Exists([]string{indexName}) + if err != nil { + return nil, fmt.Errorf("检查索引存在失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + + // 如果索引已存在,直接返回 + if res.StatusCode == 200 { + log.Printf("索引 %s 已存在\n", indexName) + detail, err := es.getIndexDetail(indexName) + if err != nil { + return nil, err + } + return detail, nil + } + // 创建索引请求 + req := esapi.IndicesCreateRequest{ + Index: indexName, // 索引名称 + Body: strings.NewReader(mapping), // 索引映射配置 + } + // 执行创建索引请求 + res, err = req.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("创建索引请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + return nil, fmt.Errorf("创建索引失败: %s", res.String()) + } + // 获取新创建索引的详情 + detail, err := es.getIndexDetail(indexName) + if err != nil { + return nil, err + } + + log.Printf("✅ 成功创建索引: %s\n", indexName) + return detail, nil +} + +// deleteIndex 删除索引 +func (es *ESClient) deleteIndex(indexName string) error { + req := esapi.IndicesDeleteRequest{ + Index: []string{indexName}, // 要删除的索引 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("删除索引请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("索引不存在: %s", indexName) + } + return fmt.Errorf("删除索引失败: %s", res.String()) + } + + log.Printf("✅ 成功删除索引: %s\n", indexName) + return nil +} + +// 更新索引设置 +func (es *ESClient) updateIndexSettings(indexName string, settings map[string]interface{}) error { + // 构建设置请求体 + requestBody := map[string]interface{}{ + "settings": settings, // 新的设置参数 + } + + bodyJSON, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("序列化设置失败: %w", err) + } + + req := esapi.IndicesPutSettingsRequest{ + Index: []string{indexName}, // 要更新的索引 + Body: strings.NewReader(string(bodyJSON)), // 设置内容 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("更新索引设置请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("索引不存在: %s", indexName) + } + return fmt.Errorf("更新索引设置失败: %s", res.String()) + } + + log.Printf("✅ 成功更新索引设置: %s\n", indexName) + return nil +} + +// 更新索引映射(添加新字段) +func (es *ESClient) updateIndexMappings(indexName string, newMappings map[string]interface{}) error { + // 构建映射请求体 + requestBody := map[string]interface{}{ + "properties": newMappings, // 新的映射字段 + } + + bodyJSON, err := json.Marshal(requestBody) + if err != nil { + return fmt.Errorf("序列化映射失败: %w", err) + } + + req := esapi.IndicesPutMappingRequest{ + Index: []string{indexName}, // 要更新的索引 + Body: strings.NewReader(string(bodyJSON)), // 映射内容 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("更新索引映射请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("索引不存在: %s", indexName) + } + return fmt.Errorf("更新索引映射失败: %s", res.String()) + } + + log.Printf("✅ 成功更新索引映射: %s\n", indexName) + return nil +} + +// 关闭索引 +func (es *ESClient) closeIndex(indexName string) error { + req := esapi.IndicesCloseRequest{ + Index: []string{indexName}, // 要关闭的索引 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("关闭索引请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("索引不存在: %s", indexName) + } + return fmt.Errorf("关闭索引失败: %s", res.String()) + } + + log.Printf("✅ 已关闭索引: %s\n", indexName) + return nil +} + +// 打开索引 +func (es *ESClient) openIndex(indexName string) error { + req := esapi.IndicesOpenRequest{ + Index: []string{indexName}, // 要打开的索引 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("打开索引请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("索引不存在: %s", indexName) + } + return fmt.Errorf("打开索引失败: %s", res.String()) + } + + log.Printf("✅ 已打开索引: %s\n", indexName) + return nil +} + +// getDocumentCount 获取索引文档数量 +func (es *ESClient) getDocumentCount(indexName string) (int64, error) { + req := esapi.CountRequest{ + Index: []string{indexName}, // 要计数的索引 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return 0, fmt.Errorf("获取文档数量请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + + if res.IsError() { + if res.StatusCode == 404 { + return 0, fmt.Errorf("索引不存在: %s", indexName) + } + return 0, fmt.Errorf("获取文档数量失败: %s", res.String()) + } + + var result map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return 0, fmt.Errorf("解析响应失败: %w", err) + } + // 提取文档数量 + if count, ok := result["count"].(float64); ok { + return int64(count), nil + } + + return 0, fmt.Errorf("无法获取文档数量") +} + +// ========================== 文档相关操作 ============= + +// createDocument 创建文档 +func (es *ESClient) createDocument(indexName string, id string, doc interface{}) error { + // 将文档序列化为JSON + docJSON, err := json.Marshal(doc) + if err != nil { + return fmt.Errorf("序列化文档失败: %w", err) + } + + req := esapi.IndexRequest{ + Index: indexName, // 目标索引 + DocumentID: id, // 文档ID(可选,为空时ES自动生成) + Body: strings.NewReader(string(docJSON)), // 文档内容 + Refresh: "true", // 立即刷新使文档可搜索 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("创建文档请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + return fmt.Errorf("创建文档失败: %s", res.String()) + } + + log.Printf("✅ 成功创建文档: indexName=%s\n", indexName) + return nil +} + +// getDocument 根据ID获取文档 +func (es *ESClient) getDocument(indexName string, id string) (map[string]interface{}, error) { + req := esapi.GetRequest{ + Index: indexName, // 索引名称 + DocumentID: id, // 文档ID + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("获取文档请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return nil, fmt.Errorf("文档不存在: ID=%s", id) + } + return nil, fmt.Errorf("获取文档失败: %s", res.String()) + } + + var result map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("解析响应失败: %w", err) + } + + return result, nil +} + +// updateDocument 更新文档 +func (es *ESClient) updateDocument(indexName string, id string, updateData map[string]interface{}) error { + // 构建更新脚本 + updateBody := map[string]interface{}{ + "doc": updateData, // 更新内容放在doc字段中 + } + + bodyJSON, err := json.Marshal(updateBody) + if err != nil { + return fmt.Errorf("序列化更新数据失败: %w", err) + } + + req := esapi.UpdateRequest{ + Index: indexName, // 索引名称 + DocumentID: id, // 文档ID + Body: strings.NewReader(string(bodyJSON)), // 更新内容 + Refresh: "true", // 立即刷新使更新可搜索 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("更新文档请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("文档不存在: ID=%s", id) + } + return fmt.Errorf("更新文档失败: %s", res.String()) + } + + log.Printf("✅ 成功更新文档: ID=%s\n", id) + return nil +} + +// deleteDocument 删除文档 +func (es *ESClient) deleteDocument(indexName string, id string) error { + req := esapi.DeleteRequest{ + Index: indexName, // 索引名称 + DocumentID: id, // 文档ID + Refresh: "true", // 立即刷新 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return fmt.Errorf("删除文档请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + if res.StatusCode == 404 { + return fmt.Errorf("文档不存在: ID=%s", id) + } + return fmt.Errorf("删除文档失败: %s", res.String()) + } + + log.Printf("✅ 成功删除文档: ID=%s\n", id) + return nil +} + +// searchDocuments 搜索文档 +func (es *ESClient) searchDocuments(indexName string, query map[string]interface{}) ([]map[string]interface{}, error) { + // 将查询条件序列化为JSON + queryJSON, err := json.Marshal(query) + if err != nil { + return nil, fmt.Errorf("序列化查询失败: %w", err) + } + + req := esapi.SearchRequest{ + Index: []string{indexName}, // 要搜索的索引 + Body: strings.NewReader(string(queryJSON)), // 查询条件 + } + + res, err := req.Do(context.Background(), es.client) + if err != nil { + return nil, fmt.Errorf("搜索请求失败: %w", err) + } + defer res.Body.Close() // 确保响应体被关闭 + // 检查响应状态 + if res.IsError() { + return nil, fmt.Errorf("搜索失败: %s", res.String()) + } + + var result map[string]interface{} + if err := json.NewDecoder(res.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("解析搜索结果失败: %w", err) + } + + // 提取命中的文档 + hits, ok := result["hits"].(map[string]interface{}) + if !ok { + return []map[string]interface{}{}, nil // 没有命中结果 + } + + hitsArray, ok := hits["hits"].([]interface{}) + if !ok { + return []map[string]interface{}{}, nil // 命中结果格式不正确 + } + // 处理每个命中文档 + var documents []map[string]interface{} + for _, hit := range hitsArray { + if hitMap, ok := hit.(map[string]interface{}); ok { + if source, ok := hitMap["_source"].(map[string]interface{}); ok { + source["_id"] = hitMap["_id"] // 将文档ID添加到结果中 + documents = append(documents, source) + } + } + } + + return documents, nil +} + +//// BulkCreate 批量创建文档 +//func (es *ESClient) BulkCreate(docs map[string]interface{}) error { +// var body string +// for id, doc := range docs { +// docJSON, err := json.Marshal(doc) +// if err != nil { +// return fmt.Errorf("序列化文档失败: %w", err) +// } +// +// // 批量操作格式 +// body += fmt.Sprintf(`{"index":{"_index":"%s","_id":"%s"}}%s`, es.index, id, "\n") +// body += string(docJSON) + "\n" +// } +// +// req := esapi.BulkRequest{ +// Body: strings.NewReader(body), +// } +// +// res, err := req.Do(context.Background(), es.client) +// if err != nil { +// return fmt.Errorf("批量操作请求失败: %w", err) +// } +// defer res.Body.Close() +// +// if res.IsError() { +// return fmt.Errorf("批量操作失败: %s", res.String()) +// } +// +// log.Printf("✅ 批量创建 %d 个文档成功\n", len(docs)) +// return nil +//} + +// ==================== 辅助函数 ==================== + +// getString 从map中安全获取字符串值,避免类型断言失败 +func getString(m map[string]interface{}, key, defaultValue string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return defaultValue // 如果key不存在或不是字符串类型,返回默认值 +} + +// =================== C 导入函数 ======================= + +// c响应信息 +type APIResponse struct { + Success bool `json:"success"` // 操作是否成功 + Message string `json:"message"` // 响应消息(错误时为错误信息) + Data interface{} `json:"data"` // 响应数据(成功时返回) +} + +// ListAllIndices 查询所有索引 +// +//export ListAllIndices +func ListAllIndices() *C.char { + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + // 查询所有索引 + info, err := client.listAllIndices() + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// GetIndicesInfo 获取所有索引的详细信息 +// +//export GetIndicesInfo +func GetIndicesInfo() *C.char { + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + // 获取所有索引的详细信息 + info, err := client.getIndicesInfo() + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// GetIndexDetail 获取单个索引的详细信息 +// +//export GetIndexDetail +func GetIndexDetail(indexName *C.char) *C.char { + goIndexName := C.GoString(indexName) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + info, err := client.getIndexDetail(goIndexName) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// CreateIndex 创建索引(如果不存在) +// +//export CreateIndex +func CreateIndex(indexName, mapping *C.char) *C.char { + goIndexName := C.GoString(indexName) + goMapping := C.GoString(mapping) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + info, err := client.createIndex(goIndexName, goMapping) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// DeleteIndex 删除索引 +// +//export DeleteIndex +func DeleteIndex(indexName *C.char) *C.char { + goIndexName := C.GoString(indexName) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + err = client.deleteIndex(goIndexName) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// GetDocumentCount 获取索引文档数量 +// +//export GetDocumentCount +func GetDocumentCount(indexName *C.char) *C.char { + goIndexName := C.GoString(indexName) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + info, err := client.getDocumentCount(goIndexName) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// CreateDocument 创建文档 +// +//export CreateDocument +func CreateDocument(indexName *C.char, id *C.char, doc *C.char) *C.char { + goIndexName := C.GoString(indexName) + goId := C.GoString(id) + goDoc := C.GoString(doc) + var apiResp APIResponse + var newDoc interface{} + if err := json.Unmarshal([]byte(goDoc), &newDoc); err != nil { + apiResp = APIResponse{ + Success: false, + Message: fmt.Sprintf("解析JSON失败: %v", err), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + err = client.createDocument(goIndexName, goId, newDoc) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// GetDocument 根据ID获取文档 +// +//export GetDocument +func GetDocument(indexName *C.char, id *C.char) *C.char { + goIndexName := C.GoString(indexName) + goId := C.GoString(id) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + info, err := client.getDocument(goIndexName, goId) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// UpdateDocument 更新文档 +// +//export UpdateDocument +func UpdateDocument(indexName *C.char, id *C.char, updateData *C.char) *C.char { + goIndexName := C.GoString(indexName) + goId := C.GoString(id) + goUpdateData := C.GoString(updateData) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + // 转换updateData + var newUpdateData map[string]interface{} + if err = json.Unmarshal([]byte(goUpdateData), &newUpdateData); err != nil { + apiResp = APIResponse{ + Success: false, + Message: fmt.Sprintf("解析JSON失败: %v", err), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + err = client.updateDocument(goIndexName, goId, newUpdateData) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// DeleteDocument 删除文档 +// +//export DeleteDocument +func DeleteDocument(indexName *C.char, id *C.char) *C.char { + goIndexName := C.GoString(indexName) + goId := C.GoString(id) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + err = client.deleteDocument(goIndexName, goId) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// SearchDocuments 搜索文档 +// +//export SearchDocuments +func SearchDocuments(indexName *C.char, query *C.char) *C.char { + goIndexName := C.GoString(indexName) + goQuery := C.GoString(query) + var apiResp APIResponse + // 创建ES客户端 + client, err := newESClient([]string{esAddress}, esUsername, esPassword) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + // 转换updateData 解析json + var newQuery map[string]interface{} + if err = json.Unmarshal([]byte(goQuery), &newQuery); err != nil { + apiResp = APIResponse{ + Success: false, + Message: fmt.Sprintf("解析JSON失败: %v", err), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + info, err := client.searchDocuments(goIndexName, newQuery) + if err != nil { + apiResp = APIResponse{ + Success: false, + Message: err.Error(), + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) + } + apiResp = APIResponse{ + Success: true, + Data: info, + } + apiRespStr, _ := json.Marshal(apiResp) + return C.CString(string(apiRespStr)) +} + +// 释放C字符串内存 +// +//export FreeCString +func FreeCString(str *C.char) { + C.free(unsafe.Pointer(str)) +} + +// main 函数 +//func main() { +//} diff --git a/es/esDll.go b/es/esDll.go new file mode 100644 index 0000000..123f45d --- /dev/null +++ b/es/esDll.go @@ -0,0 +1,133 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "syscall" + "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字符串 +} + +// 初始化esDLL +func InitEsDLL() (*esDLL, error) { + dllPath := filepath.Join("es", "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 + } +} + +// 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 +} + +// 查询所有索引 +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 +} + +func (m *esDLL) CreateDocument(indexName string, id string, doc interface{}) (string, error) { + proc, err := m.dll.FindProc("CreateDocument") + if err != nil { + return "", fmt.Errorf("找不到函数 CreateDocument: %v", err) + } + docJson, err := json.Marshal(doc) + if err != nil { + return "", err + } + indexNamePtr, _ := syscall.BytePtrFromString(indexName) + idPtr, _ := syscall.BytePtrFromString(id) + docJsonPtr, _ := syscall.BytePtrFromString(string(docJson)) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(indexNamePtr)), + uintptr(unsafe.Pointer(idPtr)), + uintptr(unsafe.Pointer(docJsonPtr)), + ) + result := m.cStr(resultPtr) + return result, nil +} + +//func main() { +// dll, err := InitEsDLL() +// if err != nil { +// fmt.Println(err) +// } +// //indices, err := dll.ListAllIndices() +// //if err != nil { +// // fmt.Println(err) +// //} +// //fmt.Println(string(indices)) +// +// doc := Document{ +// ID: "10003", +// Title: "测试文档3", +// Content: "这是一个测试文档3", +// Author: "测试员3", +// CreatedAt: time.Now(), +// Tags: []string{"测试3", "文档3"}, +// } +// document, err := dll.CreateDocument("test-cc", doc.ID, doc) +// if err != nil { +// fmt.Println(err) +// } +// fmt.Println(document) +//} diff --git a/es/main.go b/es/main.go index 373d74b..a57906f 100644 --- a/es/main.go +++ b/es/main.go @@ -1,2088 +1,2088 @@ package main -import ( - "context" - "crypto/md5" - "crypto/tls" - "database/sql" - "encoding/hex" - "encoding/json" - "fmt" - "github.com/elastic/go-elasticsearch/v8" - "github.com/elastic/go-elasticsearch/v8/esapi" - "golang.org/x/image/bmp" - "golang.org/x/image/draw" - "golang.org/x/image/tiff" - "golang.org/x/image/webp" - "image" - "image/color" - "image/jpeg" - "image/png" - "io" - "io/ioutil" - "log" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "sync/atomic" - "time" - - _ "github.com/go-sql-driver/mysql" - "github.com/nfnt/resize" -) - -// ES 配置 -const ( - esAddress = "http://103.236.91.138:9200" - esUsername = "elastic" - esPassword = "5mRDIUg52VC0fp14nw-F" - esIndex = "books-from-mysql" - ClientID = "203c5a7ba8bd4b8488d5e26f93052642" // 拼多多开放平台配置 - ClientSecret = "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" - PDDApiURL = "https://gw-upload.pinduoduo.com/api/upload" -) - -// 配置参数 -const ( - maxWorkers = 15 // 最大并发worker数量 - maxRetries = 3 // 最大重试次数 - retryDelay = 2 * time.Second // 重试延迟 - progressInterval = 5 * time.Second // 进度报告间隔 -) - -// ES 客户端封装 -type ESClient struct { - client *elasticsearch.Client -} - -// 数据库记录结构体 -type CrawlerRecord struct { - BookISBN sql.NullString - BookPicture sql.NullString -} - -// 处理结果结构体 -type ProcessResult struct { - Record CrawlerRecord - Success bool - LocalPaths []string - PDDURLs []string - Error error - WorkerID int - ProcessedAt time.Time -} - -// 全局统计 -type Statistics struct { - Total int32 - Success int32 - Failed int32 - Skipped int32 - CurrentIndex int32 - StartTime time.Time -} - -// NewESClient 初始化 ES 客户端 -// 说明:保持一致的连接方式(禁用证书校验、设置超时和连接池参数) -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, - }, - } - cli, err := elasticsearch.NewClient(cfg) - if err != nil { - return nil, err - } - return &ESClient{client: cli}, nil -} - -// CheckHealth 检查 ES 集群健康 -// 行为:等待状态至少为 yellow,输出基本信息 -func (es *ESClient) CheckHealth() error { - res, err := es.client.Cluster.Health( - es.client.Cluster.Health.WithWaitForStatus("yellow"), - es.client.Cluster.Health.WithTimeout(30*time.Second), - ) - if err != nil { - return err - } - defer res.Body.Close() - if res.IsError() { - return fmt.Errorf("Elasticsearch 健康检查失败: %s", res.String()) - } - var m map[string]interface{} - if err := json.NewDecoder(res.Body).Decode(&m); err == nil { - log.Printf("ES status=%v nodes=%v cluster=%v", m["status"], m["number_of_nodes"], m["cluster_name"]) - } - return nil -} - -// PDDImageProcessor 实现图片处理器 -// pdd上传图片官方接口 -// 上传图片到拼多多 -func uploadToPDD(token, imagePath string) (string, error) { - // 检查token是否有效 - if len(token) == 0 { - return "", fmt.Errorf("获取到的token为空") - } - result, err := ProcessAndUploadImage(imagePath, token) - if err != nil { - return "", fmt.Errorf("拼多多图片上传失败: %v", err) - } - - // 解析JSON响应获取URL - var response struct { - RequestID string `json:"request_id"` - URL string `json:"url"` - } - - err = json.Unmarshal([]byte(result), &response) - if err != nil { - return "", fmt.Errorf("解析上传响应失败: %v", err) - } - - if response.URL == "" { - return "", fmt.Errorf("上传响应中未找到URL") - } - - return response.URL, nil -} -func ProcessAndUploadImage(imagePath, token string) (string, error) { - // 打开图片文件 - file, err := os.Open(imagePath) - if err != nil { - return "", fmt.Errorf("failed to open image file: %v", err) - } - defer file.Close() - - // 准备参数 - 不包含文件路径 - params := map[string]string{ - "access_token": token, - "data_type": "JSON", - "type": "pdd.goods.img.upload", - "client_id": ClientID, - "timestamp": fmt.Sprintf("%d", time.Now().Unix()), - } - - // 生成签名(不包含文件路径) - params["sign"] = generateSign(params) - - // 创建multipart表单 - body := &strings.Builder{} - writer := multipart.NewWriter(body) - - // 写入文本参数 - for key, value := range params { - if err := writer.WriteField(key, value); err != nil { - return "", fmt.Errorf("failed to write field %s: %v", key, err) - } - } - - // 写入文件流 - 使用正确的字段名 "file" - part, err := writer.CreateFormFile("file", filepath.Base(imagePath)) - if err != nil { - return "", fmt.Errorf("failed to create form file: %v", err) - } - - if _, err := io.Copy(part, file); err != nil { - return "", fmt.Errorf("failed to copy file data: %v", err) - } - - // 关闭writer - if err := writer.Close(); err != nil { - return "", fmt.Errorf("failed to close writer: %v", err) - } - - // 创建请求 - req, err := http.NewRequest("POST", PDDApiURL, strings.NewReader(body.String())) - if err != nil { - return "", fmt.Errorf("failed to create request: %v", err) - } - - // 设置请求头 - req.Header.Set("Content-Type", writer.FormDataContentType()) - 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 "", fmt.Errorf("failed to send request: %v", err) - } - defer resp.Body.Close() - - // 读取响应 - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response: %v", err) - } - - log.Printf("拼多多API响应状态: %d", resp.StatusCode) - log.Printf("拼多多API响应内容: %s", string(respBody)) - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("API returned error status: %d, body: %s", resp.StatusCode, string(respBody)) - } - - // 解析响应 - var result map[string]interface{} - if err := json.Unmarshal(respBody, &result); err != nil { - return "", fmt.Errorf("failed to parse response: %v", err) - } - - // 检查API返回的错误 - if errorResponse, exists := result["error_response"]; exists { - errorMsg, _ := json.Marshal(errorResponse) - return "", fmt.Errorf("API returned error: %s", string(errorMsg)) - } - - // 查找成功的响应 - for key, value := range result { - if key != "error_response" { - successResponse, _ := json.Marshal(value) - return string(successResponse), nil - } - } - - return string(respBody), nil -} - -// generateSign 生成拼多多API签名 -func generateSign(params map[string]string) string { - // 按参数名排序 - var keys []string - for k := range params { - keys = append(keys, k) - } - sort.Strings(keys) - // 拼接参数字符串 - var signStr string - for _, k := range keys { - signStr += k + params[k] - } - signStr = ClientSecret + signStr + ClientSecret - // 计算MD5并转为大写 - hasher := md5.New() - hasher.Write([]byte(signStr)) - result := strings.ToUpper(hex.EncodeToString(hasher.Sum(nil))) - return result -} - -// GetPddToken 获取PDD token(简化版) -func GetPddToken() (string, error) { - url := "https://api.buzhiyushu.cn/huidiao/pdd/getPddChildrenBooksToken" - - resp, err := http.Get(url) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - // 使用map解析JSON - var result map[string]interface{} - json.Unmarshal(body, &result) - - // 检查业务状态 - if code, ok := result["code"].(float64); !ok || code != 200 { - return "", fmt.Errorf("API错误: %v", result["msg"]) - } - - // 提取token - token := result["data"].(string) - return token, nil -} - -// 并发处理记录的主要函数 -func processRecordsConcurrently(records []CrawlerRecord, imageDir string, es *ESClient, maxWorkers int, token string) []ProcessResult { - var stats Statistics - stats.Total = int32(len(records)) - stats.StartTime = time.Now() - - // 创建通道 - recordChan := make(chan CrawlerRecord, len(records)) - resultChan := make(chan ProcessResult, len(records)) - - // 启动worker - var wg sync.WaitGroup - for i := 0; i < maxWorkers; i++ { - wg.Add(1) - go worker(token, i, &wg, recordChan, resultChan, imageDir, es, &stats) - } - - // 发送任务到通道 - go func() { - for _, record := range records { - recordChan <- record - } - close(recordChan) - }() - - // 启动进度报告 - go progressReporter(&stats) - - // 收集结果 - var results []ProcessResult - go func() { - for result := range resultChan { - results = append(results, result) - } - }() - - // 等待所有worker完成 - wg.Wait() - close(resultChan) - - return results -} - -// worker 处理函数 -func worker(token string, id int, wg *sync.WaitGroup, recordChan <-chan CrawlerRecord, resultChan chan<- ProcessResult, imageDir string, es *ESClient, stats *Statistics) { - defer wg.Done() - - for record := range recordChan { - currentIndex := atomic.AddInt32(&stats.CurrentIndex, 1) - - result := ProcessResult{ - Record: record, - WorkerID: id, - ProcessedAt: time.Now(), - } - - // 检查记录有效性 - if !isRecordValid(record) { - atomic.AddInt32(&stats.Skipped, 1) - result.Success = false - result.Error = fmt.Errorf("无效记录: ISBN或图片URL为空") - resultChan <- result - continue - } - - // 处理记录(带重试机制) - var localPaths, pddURLs []string - var err error - - for attempt := 1; attempt <= maxRetries; attempt++ { - localPaths, pddURLs, err = processSingleRecord(token, record, imageDir, es) - if err == nil { - break - } - - // 如果是ES记录未找到的错误,不需要重试 - if strings.Contains(err.Error(), "ES记录未找到") { - break - } - - if attempt < maxRetries { - log.Printf("Worker %d: 第 %d 次尝试处理 ISBN %s 失败, %d 秒后重试: %v", - id, attempt, record.BookISBN.String, retryDelay/time.Second, err) - time.Sleep(retryDelay) - } - } - - if err != nil { - atomic.AddInt32(&stats.Failed, 1) - result.Success = false - result.Error = err - // 即使失败,也记录已处理的本地路径(如果有) - result.LocalPaths = localPaths - result.PDDURLs = pddURLs - - // 根据错误类型记录不同的日志 - if strings.Contains(err.Error(), "ES记录未找到") { - log.Printf("Worker %d: ES记录未找到 [%d/%d] ISBN: %s", - id, currentIndex, stats.Total, record.BookISBN.String) - } else { - log.Printf("Worker %d: 处理失败 [%d/%d] ISBN: %s, 错误: %v", - id, currentIndex, stats.Total, record.BookISBN.String, err) - } - } else { - atomic.AddInt32(&stats.Success, 1) - result.Success = true - result.LocalPaths = localPaths - result.PDDURLs = pddURLs - - log.Printf("Worker %d: 成功处理 [%d/%d] ISBN: %s, 生成 %d 个文件, 上传 %d 个URL", - id, currentIndex, stats.Total, record.BookISBN.String, len(localPaths), len(pddURLs)) - } - - resultChan <- result - } -} - -// 检查记录有效性 -func isRecordValid(record CrawlerRecord) bool { - if !record.BookISBN.Valid || record.BookISBN.String == "" { - return false - } - if !record.BookPicture.Valid || record.BookPicture.String == "" { - return false - } - return true -} - -// 进度报告器 -func progressReporter(stats *Statistics) { - ticker := time.NewTicker(progressInterval) - defer ticker.Stop() - - for range ticker.C { - processed := atomic.LoadInt32(&stats.CurrentIndex) - success := atomic.LoadInt32(&stats.Success) - failed := atomic.LoadInt32(&stats.Failed) - skipped := atomic.LoadInt32(&stats.Skipped) - - elapsed := time.Since(stats.StartTime) - rate := float64(processed) / elapsed.Seconds() - - // 计算预估剩余时间 - var eta time.Duration - if processed > 0 && rate > 0 { - remaining := float64(stats.Total - processed) - eta = time.Duration(remaining/rate) * time.Second - } - - fmt.Printf("[进度] 已处理: %d/%d (成功: %d, 失败: %d, 跳过: %d) | 速率: %.2f 条/秒 | 运行: %v | ETA: %v\n", - processed, stats.Total, success, failed, skipped, rate, elapsed.Round(time.Second), eta.Round(time.Second)) - - if processed >= stats.Total { - break - } - } -} - -// 打印最终统计 -func printFinalStatistics(results []ProcessResult) { - var success, failed, skipped int - var totalFilesGenerated int - var totalURLsUploaded int - - // 失败原因分类 - failureReasons := make(map[string]int) - - for _, result := range results { - if result.Success { - success++ - totalFilesGenerated += len(result.LocalPaths) - totalURLsUploaded += len(result.PDDURLs) - } else if result.Error != nil && strings.Contains(result.Error.Error(), "无效记录") { - skipped++ - failureReasons["无效记录(ISBN或URL为空)"]++ - } else { - failed++ - // 即使是失败的情况,也可能生成了部分文件 - totalFilesGenerated += len(result.LocalPaths) - totalURLsUploaded += len(result.PDDURLs) - - // 分类失败原因 - errMsg := result.Error.Error() - switch { - case strings.Contains(errMsg, "ES记录未找到"): - failureReasons["ES记录未找到"]++ - case strings.Contains(errMsg, "查询ES中ID失败"): - failureReasons["ES查询失败"]++ - case strings.Contains(errMsg, "下载图片失败"): - failureReasons["图片下载失败"]++ - case strings.Contains(errMsg, "处理图片失败"): - failureReasons["图片处理失败"]++ - case strings.Contains(errMsg, "上传PNG图片失败"): - failureReasons["PNG上传失败"]++ - case strings.Contains(errMsg, "上传JPG图片失败"): - failureReasons["JPG上传失败"]++ - case strings.Contains(errMsg, "更新ES数据失败"): - failureReasons["ES更新失败"]++ - default: - failureReasons["其他错误"]++ - } - } - } - - fmt.Printf("\n=== 处理完成 ===\n") - fmt.Printf("总记录数: %d\n", len(results)) - fmt.Printf("成功: %d\n", success) - fmt.Printf("失败: %d\n", failed) - fmt.Printf("跳过: %d\n", skipped) - fmt.Printf("成功率: %.2f%%\n", float64(success)/float64(len(results))*100) - fmt.Printf("生成文件总数: %d (平均每条记录 %.1f 个文件)\n", totalFilesGenerated, float64(totalFilesGenerated)/float64(len(results))) - fmt.Printf("上传URL总数: %d (平均每条记录 %.1f 个URL)\n", totalURLsUploaded, float64(totalURLsUploaded)/float64(len(results))) - - // 显示失败原因统计 - if len(failureReasons) > 0 { - fmt.Printf("\n=== 失败原因统计 ===\n") - for reason, count := range failureReasons { - fmt.Printf(" %s: %d\n", reason, count) - } - } - - // 显示处理详情示例 - fmt.Printf("\n=== 处理详情示例 ===\n") - successCount := 0 - failedCount := 0 - for _, result := range results { - if result.Success && successCount < 3 { - fmt.Printf("✅ 成功: ISBN %s -> 文件: %d 个, URL: %d 个\n", - result.Record.BookISBN.String, - len(result.LocalPaths), - len(result.PDDURLs)) - successCount++ - } else if !result.Success && failedCount < 3 && !strings.Contains(result.Error.Error(), "无效记录") { - fmt.Printf("❌ 失败: ISBN %s -> 错误: %v\n", - result.Record.BookISBN.String, - result.Error) - failedCount++ - } - if successCount >= 3 && failedCount >= 3 { - break - } - } -} - -// 处理单条记录 -func processSingleRecord(token string, record CrawlerRecord, imageDir string, es *ESClient) ([]string, []string, error) { - // 更新ES - ids, err := es.FindIDsByISBN(esIndex, record.BookISBN.String) - if err != nil { - return nil, nil, fmt.Errorf("查询ES中ID失败: %v", err) - } - var pngImageUrl string - var jpgImageUrl string - var localPaths []string - var pddURLs []string - if ids != "" { - // 下载并处理图片 - pngPath, jpgPath, err := processAndSaveImage(record, imageDir) - if err != nil { - err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) - if err != nil { - log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) - } else { - log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) - } - return nil, nil, fmt.Errorf("处理图片失败: %v", err) - } - localPaths = []string{pngPath, jpgPath} - // 上传到PDD - pngImageUrl, err = uploadToPDD(token, pngPath) - if err != nil { - err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) - if err != nil { - log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) - } else { - log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) - } - return nil, nil, fmt.Errorf("上传PNG图片失败: %v", err) - } - // 上传到PDD - jpgImageUrl, err = uploadToPDD(token, jpgPath) - if err != nil { - err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) - if err != nil { - log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) - } else { - log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) - } - return nil, nil, fmt.Errorf("上传JPG图片失败: %v", err) - } - pddURLs = []string{pngImageUrl, jpgImageUrl} - err = es.UpdateBookPicsByID(esIndex, ids, "", pngImageUrl, jpgImageUrl) - if err != nil { - err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) - if err != nil { - log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) - } else { - log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) - } - return nil, nil, fmt.Errorf("更新ES数据失败: %v", err) - } - - for _, path := range localPaths { - // ES更新成功后删除本地图片 - if removeErr := os.Remove(path); removeErr == nil { - log.Printf("ES更新成功,已删除本地图片: %s", path) - } else { - log.Printf("警告: 无法删除本地图片 %s: %v", path, removeErr) - } - } - } else { - // ids为空,将ISBN存储到txt文件 - err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) - if err != nil { - log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) - } else { - log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) - } - return nil, nil, fmt.Errorf("未找到ISBN %s 对应的ES记录", record.BookISBN.String) - } - return localPaths, pddURLs, nil -} - -// 保存未找到的ISBN和图片URL到txt文件(CSV格式,带去重) -func saveISBNToFile(isbn string, imageUrl string) error { - filename := "cmd/update_es_gt/xgy_not_found_isbns.txt" - - // 读取现有内容检查是否已存在 - existingRecords := make(map[string]bool) - if content, err := os.ReadFile(filename); err == nil { - lines := strings.Split(string(content), "\n") - for _, line := range lines { - if line != "" && !strings.HasPrefix(line, "#") { - parts := strings.Split(line, ",") - if len(parts) > 0 { - existingRecords[parts[0]] = true // 以ISBN作为去重依据 - } - } - } - } - - // 如果已存在,则不重复添加 - if existingRecords[isbn] { - return nil - } - // 以追加模式打开文件 - file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return fmt.Errorf("打开文件失败: %v", err) - } - defer file.Close() - // 如果是空文件,先写入CSV表头 - stat, err := file.Stat() - if err == nil && stat.Size() == 0 { - header := "# ISBN,ImageURL\n" - if _, err := file.WriteString(header); err != nil { - return fmt.Errorf("写入表头失败: %v", err) - } - } - - // 写入ISBN和图片URL,用逗号分隔,并添加换行符 - line := fmt.Sprintf("%s,%s\n", isbn, imageUrl) - _, err = file.WriteString(line) - if err != nil { - return fmt.Errorf("写入文件失败: %v", err) - } - - return nil -} - -// 下载并处理图片 -func processAndSaveImage(record CrawlerRecord, saveDir string) (string, string, error) { - // 下载图片 - img, originalFormat, err := downloadImage(record.BookPicture.String) - if err != nil { - return "", "", fmt.Errorf("下载图片失败: %v", err) - } - - fmt.Printf("下载成功,原始格式: %s\n", originalFormat) - - // 调整图片高度为600,等比例缩放 - //resizedImg := resizeImageToHeight(img, 600) - // 使用高质量缩放调整图片高度为600,等比例缩放 - resizedImg := resizeToHeightHighQuality(img, 600) - fmt.Printf("缩放后尺寸: %dx%d\n", resizedImg.Bounds().Dx(), resizedImg.Bounds().Dy()) - - // 创建800x800的透明背景 - finalImg := createCenteredImage(resizedImg, 800, 800, true) - - // 创建800x800的白色背景(用于JPG) - whiteImg := createCenteredImage(resizedImg, 800, 800, false) - - // 生成文件名 - filename := fmt.Sprintf("%s", record.BookISBN.String) - // 清理文件名中的非法字符 - filename = sanitizeFilename(filename) - - // PNG文件路径 - pngPath := filepath.Join(saveDir, filename+".png") - // JPG文件路径 - jpgPath := filepath.Join(saveDir, filename+".jpg") - - // 保存为PNG图片 - err = savePNG(finalImg, pngPath) - if err != nil { - return "", "", fmt.Errorf("保存图片失败: %v", err) - } - - // 保存为JPG图片(白色背景) - err = saveJPG(whiteImg, jpgPath, 95) // 95%质量 - if err != nil { - return "", "", fmt.Errorf("保存JPG图片失败: %v", err) - } - - fmt.Printf("转换成功: %s -> %s, 保存路径: %s\n", originalFormat, "PNG", pngPath) - fmt.Printf("转换成功: %s -> %s, 保存路径: %s\n", originalFormat, "JPG", jpgPath) - return pngPath, jpgPath, nil -} - -// 下载图片 -func downloadImage(url string) (image.Image, string, error) { - // 创建HTTP客户端,设置超时等参数 - client := &http.Client{ - Timeout: 30 * time.Second, - } - - resp, err := client.Get(url) - if err != nil { - return nil, "", err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode) - } - - // 读取响应体前几个字节来判断图片格式 - peekBytes := make([]byte, 512) - n, err := resp.Body.Read(peekBytes) - if err != nil && err != io.EOF { - return nil, "", err - } - - // 创建一个新的Reader,包含已读取的数据和剩余数据 - reader := io.MultiReader(strings.NewReader(string(peekBytes[:n])), resp.Body) - - // 根据文件头识别图片格式 - contentType := http.DetectContentType(peekBytes[:n]) - fmt.Printf("检测到的Content-Type: %s\n", contentType) - - var img image.Image - var format string - - // 根据Content-Type或文件扩展名选择解码器 - switch { - case strings.Contains(contentType, "jpeg") || strings.HasSuffix(strings.ToLower(url), ".jpg") || strings.HasSuffix(strings.ToLower(url), ".jpeg"): - img, err = jpeg.Decode(reader) - format = "JPEG" - case strings.Contains(contentType, "png") || strings.HasSuffix(strings.ToLower(url), ".png"): - img, err = png.Decode(reader) - format = "PNG" - case strings.Contains(contentType, "webp") || strings.HasSuffix(strings.ToLower(url), ".webp"): - img, err = webp.Decode(reader) - format = "WEBP" - case strings.Contains(contentType, "bmp") || strings.HasSuffix(strings.ToLower(url), ".bmp"): - img, err = bmp.Decode(reader) - format = "BMP" - case strings.Contains(contentType, "tiff") || strings.HasSuffix(strings.ToLower(url), ".tiff") || strings.HasSuffix(strings.ToLower(url), ".tif"): - img, err = tiff.Decode(reader) - format = "TIFF" - default: - // 尝试通用解码 - img, format, err = image.Decode(reader) - if err != nil { - return nil, "", fmt.Errorf("不支持的图片格式: %s, 错误: %v", contentType, err) - } - } - - if err != nil { - return nil, "", fmt.Errorf("解码图片失败: %v", err) - } - - return img, format, nil -} - -// 高质量等比例缩放到指定高度 -func resizeToHeightHighQuality(src image.Image, targetHeight int) image.Image { - bounds := src.Bounds() - srcWidth := bounds.Dx() - srcHeight := bounds.Dy() - - // 如果原图高度已经小于等于目标高度,且宽度合适,可以直接返回 - //if srcHeight <= targetHeight { - // return src - //} - - // 计算等比例缩放后的宽度 - targetWidth := uint(float64(srcWidth) * float64(targetHeight) / float64(srcHeight)) - - // 使用 Lanczos3 插值算法进行高质量缩放 - return resize.Resize(targetWidth, uint(targetHeight), src, resize.Lanczos3) -} - -// 创建居中图片(将原图放在指定大小的透明背景中央) -func createCenteredImage(src image.Image, width, height int, transparent bool) *image.RGBA { - // 创建透明背景 - dst := image.NewRGBA(image.Rect(0, 0, width, height)) - - // 设置背景颜色 - var bgColor color.Color - if transparent { - bgColor = color.RGBA{0, 0, 0, 0} // 透明 - } else { - bgColor = color.RGBA{255, 255, 255, 255} // 白色 - } - - // 填充透明背景 - //transparent := color.RGBA{0, 0, 0, 0} - draw.Draw(dst, dst.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src) - - // 计算居中位置 - srcBounds := src.Bounds() - srcWidth := srcBounds.Dx() - srcHeight := srcBounds.Dy() - - x := (width - srcWidth) / 2 - y := (height - srcHeight) / 2 - - // 将原图绘制到中央 - draw.Draw(dst, image.Rect(x, y, x+srcWidth, y+srcHeight), src, image.Point{}, draw.Over) - - return dst -} - -// 保存为PNG图片 -func savePNG(img image.Image, filename string) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - - return png.Encode(file, img) -} - -// 保存为JPG图片 -func saveJPG(img image.Image, filename string, quality int) error { - file, err := os.Create(filename) - if err != nil { - return err - } - defer file.Close() - - // 设置JPEG编码选项 - options := &jpeg.Options{ - Quality: quality, // 1-100,越高质量越好 - } - - return jpeg.Encode(file, img, options) -} - -// 清理文件名中的非法字符 -func sanitizeFilename(filename string) string { - // 替换Windows文件名中不允许的字符 - invalidChars := []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"} - for _, char := range invalidChars { - filename = strings.ReplaceAll(filename, char, "_") - } - // 移除或替换其他可能的问题字符 - filename = strings.TrimSpace(filename) - if filename == "" { - filename = "unknown" - } - return filename -} - -// 从数据库获取记录 -func getRecords(db *sql.DB) ([]CrawlerRecord, error) { - // 查询所有记录,包括 NULL 值 - query := "SELECT book_isbn, book_picture FROM dk_crawler_record_info" - rows, err := db.Query(query) - if err != nil { - return nil, err - } - defer rows.Close() - - var records []CrawlerRecord - for rows.Next() { - var record CrawlerRecord - // 使用 sql.NullString 来接收可能为 NULL 的字段 - err := rows.Scan(&record.BookISBN, &record.BookPicture) - if err != nil { - fmt.Printf("扫描记录失败: %v\n", err) - continue - } - records = append(records, record) - } - - // 检查遍历过程中是否有错误 - if err = rows.Err(); err != nil { - return nil, err - } - - return records, nil -} - -// FindIDsByISBN 根据 ISBN 查询文档 ID 列表 -func (es *ESClient) FindIDsByISBN(index, isbn string) (string, error) { - q := map[string]interface{}{ - "query": map[string]interface{}{ - "term": map[string]interface{}{"isbn": isbn}, - }, - "_source": false, - "size": 1000, - } - b, _ := json.Marshal(q) - res, err := es.client.Search( - es.client.Search.WithIndex(index), - es.client.Search.WithBody(strings.NewReader(string(b))), - es.client.Search.WithContext(context.Background()), - ) - if err != nil { - return "", err - } - defer res.Body.Close() - if res.IsError() { - return "", fmt.Errorf("搜索失败: %s", res.String()) - } - var r map[string]interface{} - if err := json.NewDecoder(res.Body).Decode(&r); err != nil { - return "", err - } - hits, _ := r["hits"].(map[string]interface{}) - arr, _ := hits["hits"].([]interface{}) - var ids string - for _, h := range arr { - m, _ := h.(map[string]interface{}) - id, _ := m["_id"].(string) - if id != "" { - //ids = append(ids, id) - ids = id - } - } - return ids, nil -} - -func (es *ESClient) UpdateBookPicsByID(index, id, localImageS, pngImageUrl, jpgImageUrl string) error { - bookPicJSON, err := json.Marshal(map[string]string{ - "localPath": localImageS, - "pddPath": jpgImageUrl, - }) - if err != nil { - return fmt.Errorf("序列化 book_pic_w 失败: %w", err) - } - - bookPicBJSON, err := json.Marshal(map[string]string{ - "localPath": localImageS, - "pddResponse": pngImageUrl, - }) - if err != nil { - return fmt.Errorf("序列化 book_pic_b 失败: %w", err) - } - // 构建更新文档 - payload := map[string]interface{}{ - "doc": map[string]string{ - "book_pic": string(bookPicJSON), - "book_pic_b": string(bookPicBJSON), - }, - } - // JSON 序列化整个更新请求 - body, err := json.Marshal(payload) - if err != nil { - return fmt.Errorf("序列化更新请求失败: %w", err) - } - req := esapi.UpdateRequest{ - Index: index, - DocumentID: id, - Body: strings.NewReader(string(body)), - } - res, err := req.Do(context.Background(), es.client) - if err != nil { - return err - } - defer res.Body.Close() - if res.IsError() { - data, _ := io.ReadAll(res.Body) - return fmt.Errorf("ES 更新失败: %s", data) - } - return nil -} - -// 从 sql.NullString 获取字符串值 -func getStringValue(nullString sql.NullString) string { - if nullString.Valid { - return nullString.String - } - return "NULL" -} - -func main() { - //// 获取token - //token, err := GetPddToken() - //if err != nil { - // fmt.Errorf("获取拼多多token失败: %v", err) - //} - //fmt.Println("token=", token) - //// 数据源名称格式 - //dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", - // "root", - // "123456", - // "localhost", - // 3306, - // "book_image") - //db, err := sql.Open("mysql", dsn) - //if err != nil { - // fmt.Printf("打开数据库连接失败: %v", err) - //} - //// 设置连接池参数 - //db.SetMaxOpenConns(20) // 最大打开连接数 - //db.SetMaxIdleConns(10) - //err = db.Ping() - //if err != nil { - // fmt.Printf("数据库连接测试失败: %v", err) - //} - // - //// 查询数据 - //records, err := getRecords(db) - //if err != nil { - // fmt.Printf("查询失败: %v", err) - //} - //imageDir := "D:\\image" - //err = os.MkdirAll(imageDir, 0755) - //if err != nil { - // fmt.Sprintf("创建目录失败: %v", err) - //} - //fmt.Printf("找到 %d 条记录需要处理\n", len(records)) - // - //es, err := NewESClient([]string{esAddress}, esUsername, esPassword) - //if err != nil { - // log.Fatalf("ES 连接失败: %v", err) - //} - //if err := es.CheckHealth(); err != nil { - // log.Fatalf("ES 健康检查失败: %v", err) - //} - //// 启动并发处理 - //results := processRecordsConcurrently(records, imageDir, es, maxWorkers, token) - // - //// 输出最终统计 - //printFinalStatistics(results) - - //mainQuerySaleISBNs() - //mainFindESOnlyISBNs() - mainQuerySaleISBNsWithEmptyPic() -} - -// 查询并导出有销售记录且book_pic字符串中pddPath为空的ISBN -func queryAndExportSaleISBNs(es *ESClient, outputFile string) error { - log.Printf("开始查询有销售记录且book_pic字符串中pddPath为空的ISBN...") - - // 使用其他字段排序,比如 isbn 字段或者时间字段 - query := map[string]interface{}{ - "query": map[string]interface{}{ - "bool": map[string]interface{}{ - "must": []map[string]interface{}{ - { - "bool": map[string]interface{}{ - "should": []map[string]interface{}{ - {"range": map[string]interface{}{"day_sale_7": map[string]interface{}{"gt": 0}}}, - {"range": map[string]interface{}{"day_sale_15": map[string]interface{}{"gt": 0}}}, - {"range": map[string]interface{}{"day_sale_30": map[string]interface{}{"gt": 0}}}, - {"range": map[string]interface{}{"day_sale_60": map[string]interface{}{"gt": 0}}}, - }, - "minimum_should_match": 1, - }, - }, - { - "bool": map[string]interface{}{ - "should": []map[string]interface{}{ - // 匹配 pddPath:"" 的JSON字符串 - {"regexp": map[string]interface{}{"book_pic": ".*\"pddPath\":\"\".*"}}, - // 匹配 pddPath: "" (带空格的) - {"regexp": map[string]interface{}{"book_pic": ".*\"pddPath\":\\s*\"\".*"}}, - // 匹配整个book_pic字段为空 - {"term": map[string]interface{}{"book_pic": ""}}, - // 匹配book_pic字段不存在 - { - "bool": map[string]interface{}{ - "must_not": map[string]interface{}{ - "exists": map[string]interface{}{"field": "book_pic"}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "_source": []string{"isbn"}, - "sort": []map[string]interface{}{ - {"isbn": "asc"}, // 使用 isbn 字段排序,或者使用其他可排序字段 - }, - "size": 10000, - } - - // 打印查询条件用于验证 - queryJSON, _ := json.MarshalIndent(query, "", " ") - log.Printf("查询条件:\n%s", string(queryJSON)) - - var allISBNs []string - var searchAfter interface{} - totalCount := 0 - page := 1 - - for { - // 复制基础查询 - currentQuery := make(map[string]interface{}) - for k, v := range query { - currentQuery[k] = v - } - - // 添加游标 - if searchAfter != nil { - currentQuery["search_after"] = searchAfter - } - - body, err := json.Marshal(currentQuery) - if err != nil { - return fmt.Errorf("序列化查询失败: %w", err) - } - - log.Printf("执行第 %d 页查询...", page) - - // 执行搜索 - res, err := es.client.Search( - es.client.Search.WithIndex(esIndex), - es.client.Search.WithBody(strings.NewReader(string(body))), - es.client.Search.WithContext(context.Background()), - ) - if err != nil { - return fmt.Errorf("ES搜索失败: %w", err) - } - defer res.Body.Close() - - if res.IsError() { - bodyBytes, _ := io.ReadAll(res.Body) - return fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) - } - - // 读取并解析响应体 - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("读取响应体失败: %w", err) - } - - var result map[string]interface{} - if err := json.Unmarshal(bodyBytes, &result); err != nil { - return fmt.Errorf("解析ES响应失败: %w", err) - } - - // 检查是否有错误 - if errMsg, exists := result["error"]; exists { - return fmt.Errorf("ES返回错误: %v", errMsg) - } - - hits, ok := result["hits"].(map[string]interface{}) - if !ok { - return fmt.Errorf("无法解析hits字段") - } - - // 获取总命中数 - if totalHits, exists := hits["total"].(map[string]interface{}); exists { - if totalValue, exists := totalHits["value"]; exists { - log.Printf("ES返回总命中数: %.0f", totalValue) - } - } - - hitList, ok := hits["hits"].([]interface{}) - if !ok || len(hitList) == 0 { - log.Printf("第 %d 页没有数据,查询完成", page) - break // 没有更多数据 - } - - // 处理当前批次的数据 - batchCount := 0 - for _, hit := range hitList { - hitMap, ok := hit.(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析hit数据") - continue - } - - source, ok := hitMap["_source"].(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析_source字段") - continue - } - - isbn, ok := source["isbn"].(string) - if ok && isbn != "" { - allISBNs = append(allISBNs, isbn) - batchCount++ - } else { - log.Printf("警告: 跳过空的ISBN字段") - } - - // 更新游标(使用最后一个文档的排序值) - sortValues, ok := hitMap["sort"].([]interface{}) - if ok && len(sortValues) > 0 { - searchAfter = sortValues - } - } - - totalCount += batchCount - log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) - page++ - - // 如果返回的数量小于请求的数量,说明已经是最后一页 - if len(hitList) < 10000 { - log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) - break - } - - // 添加短暂延迟,避免对ES造成过大压力 - time.Sleep(100 * time.Millisecond) - } - - if len(allISBNs) == 0 { - return fmt.Errorf("没有找到符合条件的ISBN记录") - } - - // 去重 - isbnSet := make(map[string]bool) - uniqueISBNs := make([]string, 0) - for _, isbn := range allISBNs { - if !isbnSet[isbn] { - isbnSet[isbn] = true - uniqueISBNs = append(uniqueISBNs, isbn) - } - } - - log.Printf("去重前: %d 条, 去重后: %d 条", len(allISBNs), len(uniqueISBNs)) - - // 确保输出目录存在 - outputDir := filepath.Dir(outputFile) - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("创建输出目录失败: %w", err) - } - - // 写入文件 - file, err := os.Create(outputFile) - if err != nil { - return fmt.Errorf("创建文件失败: %w", err) - } - defer file.Close() - - // 写入文件头信息 - header := fmt.Sprintf(`# 有销售记录且book_pic字符串中pddPath为空的ISBN列表 -# 查询条件: (day_sale_7 > 0 OR day_sale_15 > 0 OR day_sale_30 > 0 OR day_sale_60 > 0) AND (book_pic包含"pddPath":"" 或 book_pic为空 或 book_pic字段不存在) -# 索引: %s -# 统计时间: %s -# 总记录数: %d - -`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(uniqueISBNs)) - - if _, err := file.WriteString(header); err != nil { - return fmt.Errorf("写入文件头失败: %w", err) - } - - // 按字母顺序排序后写入 - sort.Strings(uniqueISBNs) - successCount := 0 - for _, isbn := range uniqueISBNs { - if _, err := file.WriteString(isbn + "\n"); err != nil { - log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) - continue - } - successCount++ - } - - log.Printf("成功导出 %d/%d 个有销售记录且book_pic字符串中pddPath为空的ISBN到文件: %s", successCount, len(uniqueISBNs), outputFile) - return nil -} - -// 查询并导出销售ISBN的主函数 -func mainQuerySaleISBNs() { - // 初始化ES客户端 - es, err := NewESClient([]string{esAddress}, esUsername, esPassword) - if err != nil { - log.Fatalf("ES连接失败: %v", err) - } - - // 检查ES健康状态 - if err := es.CheckHealth(); err != nil { - log.Fatalf("ES健康检查失败: %v", err) - } - - // 输出文件路径 - outputFile := "cmd/update_es_gt/all_isbns.txt" - - // 查询并导出ISBN - startTime := time.Now() - if err := exportAllISBNs(es, outputFile); err != nil { - log.Fatalf("导出销售ISBN失败: %v", err) - } - - elapsed := time.Since(startTime) - log.Printf("任务完成!耗时: %v,ISBN已导出到: %s", elapsed.Round(time.Millisecond), outputFile) -} - -// 导出所有ISBN到txt文件 -func exportAllISBNs(es *ESClient, outputFile string) error { - log.Printf("开始导出所有ISBN...") - - // 查询所有包含isbn字段的文档 - query := map[string]interface{}{ - "query": map[string]interface{}{ - "exists": map[string]interface{}{ - "field": "isbn", - }, - }, - "_source": []string{"isbn"}, - "sort": []map[string]interface{}{ - {"isbn": "asc"}, // 按ISBN排序 - }, - "size": 10000, - } - - var allISBNs []string - var searchAfter interface{} - totalCount := 0 - page := 1 - - for { - // 复制基础查询 - currentQuery := make(map[string]interface{}) - for k, v := range query { - currentQuery[k] = v - } - - // 添加游标 - if searchAfter != nil { - currentQuery["search_after"] = searchAfter - } - - body, err := json.Marshal(currentQuery) - if err != nil { - return fmt.Errorf("序列化查询失败: %w", err) - } - - log.Printf("执行第 %d 页查询...", page) - - // 执行搜索 - res, err := es.client.Search( - es.client.Search.WithIndex(esIndex), - es.client.Search.WithBody(strings.NewReader(string(body))), - es.client.Search.WithContext(context.Background()), - ) - if err != nil { - return fmt.Errorf("ES搜索失败: %w", err) - } - defer res.Body.Close() - - if res.IsError() { - bodyBytes, _ := io.ReadAll(res.Body) - return fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) - } - - // 读取并解析响应体 - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("读取响应体失败: %w", err) - } - - var result map[string]interface{} - if err := json.Unmarshal(bodyBytes, &result); err != nil { - return fmt.Errorf("解析ES响应失败: %w", err) - } - - // 检查是否有错误 - if errMsg, exists := result["error"]; exists { - return fmt.Errorf("ES返回错误: %v", errMsg) - } - - hits, ok := result["hits"].(map[string]interface{}) - if !ok { - return fmt.Errorf("无法解析hits字段") - } - - // 获取总命中数 - if totalHits, exists := hits["total"].(map[string]interface{}); exists { - if totalValue, exists := totalHits["value"]; exists { - if page == 1 { - log.Printf("ES索引中共有 %.0f 条包含ISBN的记录", totalValue) - } - } - } - - hitList, ok := hits["hits"].([]interface{}) - if !ok || len(hitList) == 0 { - log.Printf("第 %d 页没有数据,查询完成", page) - break // 没有更多数据 - } - - // 处理当前批次的数据 - batchCount := 0 - for _, hit := range hitList { - hitMap, ok := hit.(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析hit数据") - continue - } - - source, ok := hitMap["_source"].(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析_source字段") - continue - } - - isbn, ok := source["isbn"].(string) - if ok && isbn != "" { - allISBNs = append(allISBNs, isbn) - batchCount++ - } else { - log.Printf("警告: 跳过空的ISBN字段") - } - - // 更新游标(使用最后一个文档的排序值) - sortValues, ok := hitMap["sort"].([]interface{}) - if ok && len(sortValues) > 0 { - searchAfter = sortValues - } - } - - totalCount += batchCount - log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) - page++ - - // 如果返回的数量小于请求的数量,说明已经是最后一页 - if len(hitList) < 10000 { - log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) - break - } - - // 添加短暂延迟,避免对ES造成过大压力 - time.Sleep(100 * time.Millisecond) - } - - if len(allISBNs) == 0 { - return fmt.Errorf("没有找到包含ISBN字段的记录") - } - - // 去重 - isbnSet := make(map[string]bool) - uniqueISBNs := make([]string, 0) - for _, isbn := range allISBNs { - if !isbnSet[isbn] { - isbnSet[isbn] = true - uniqueISBNs = append(uniqueISBNs, isbn) - } - } - - log.Printf("去重前: %d 条, 去重后: %d 条", len(allISBNs), len(uniqueISBNs)) - - // 确保输出目录存在 - outputDir := filepath.Dir(outputFile) - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("创建输出目录失败: %w", err) - } - - // 写入文件 - file, err := os.Create(outputFile) - if err != nil { - return fmt.Errorf("创建文件失败: %w", err) - } - defer file.Close() - - // 写入文件头信息 - header := fmt.Sprintf(`# 所有ISBN列表 -# 索引: %s -# 导出时间: %s -# 总记录数: %d - -`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(uniqueISBNs)) - - if _, err := file.WriteString(header); err != nil { - return fmt.Errorf("写入文件头失败: %w", err) - } - - // 按字母顺序排序后写入 - sort.Strings(uniqueISBNs) - successCount := 0 - for _, isbn := range uniqueISBNs { - if _, err := file.WriteString(isbn + "\n"); err != nil { - log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) - continue - } - successCount++ - } - - log.Printf("成功导出 %d/%d 个ISBN到文件: %s", successCount, len(uniqueISBNs), outputFile) - return nil -} - -// 从ES获取所有ISBN -func getAllISBNsFromES(es *ESClient) ([]string, error) { - log.Printf("开始从ES索引 %s 获取所有ISBN...", esIndex) - - var allISBNs []string - var searchAfter interface{} - totalCount := 0 - page := 1 - - for { - query := map[string]interface{}{ - "query": map[string]interface{}{ - "exists": map[string]interface{}{ - "field": "isbn", - }, - }, - "_source": []string{"isbn"}, - "sort": []map[string]interface{}{ - {"isbn": "asc"}, - }, - "size": 10000, - } - - // 添加游标 - if searchAfter != nil { - query["search_after"] = searchAfter - } - - body, err := json.Marshal(query) - if err != nil { - return nil, fmt.Errorf("序列化查询失败: %w", err) - } - - log.Printf("执行第 %d 页ES查询...", page) - - // 执行搜索 - res, err := es.client.Search( - es.client.Search.WithIndex(esIndex), - es.client.Search.WithBody(strings.NewReader(string(body))), - es.client.Search.WithContext(context.Background()), - ) - if err != nil { - return nil, fmt.Errorf("ES搜索失败: %w", err) - } - defer res.Body.Close() - - if res.IsError() { - bodyBytes, _ := io.ReadAll(res.Body) - return nil, fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) - } - - // 解析响应 - var result map[string]interface{} - if err := json.NewDecoder(res.Body).Decode(&result); err != nil { - return nil, fmt.Errorf("解析ES响应失败: %w", err) - } - - hits, ok := result["hits"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("无法解析hits字段") - } - - hitList, ok := hits["hits"].([]interface{}) - if !ok || len(hitList) == 0 { - log.Printf("第 %d 页没有数据,查询完成", page) - break - } - - // 处理当前批次的数据 - batchCount := 0 - for _, hit := range hitList { - hitMap, ok := hit.(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析hit数据") - continue - } - - source, ok := hitMap["_source"].(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析_source字段") - continue - } - - isbn, ok := source["isbn"].(string) - if ok && isbn != "" { - allISBNs = append(allISBNs, isbn) - batchCount++ - } - - // 更新游标 - sortValues, ok := hitMap["sort"].([]interface{}) - if ok && len(sortValues) > 0 { - searchAfter = sortValues - } - } - - totalCount += batchCount - log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) - page++ - - // 如果返回的数量小于请求的数量,说明已经是最后一页 - if len(hitList) < 10000 { - log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) - break - } - - time.Sleep(100 * time.Millisecond) - } - - if len(allISBNs) == 0 { - return nil, fmt.Errorf("ES中没有找到包含ISBN字段的记录") - } - - log.Printf("从ES中获取到 %d 个ISBN", len(allISBNs)) - return allISBNs, nil -} - -// 批量检查ISBN在数据库中是否存在 -func checkISBNsInDB(db *sql.DB, isbns []string) (map[string]bool, error) { - log.Printf("开始检查 %d 个ISBN在数据库中的存在情况...", len(isbns)) - - existsMap := make(map[string]bool) - - // 分批处理,避免SQL语句过长 - batchSize := 1000 - totalBatches := (len(isbns) + batchSize - 1) / batchSize - - for batch := 0; batch < totalBatches; batch++ { - start := batch * batchSize - end := start + batchSize - if end > len(isbns) { - end = len(isbns) - } - - batchISBNs := isbns[start:end] - log.Printf("处理数据库批次 %d/%d: ISBN范围 %d-%d", batch+1, totalBatches, start+1, end) - - // 构建IN查询的占位符 - placeholders := make([]string, len(batchISBNs)) - args := make([]interface{}, len(batchISBNs)) - for i, isbn := range batchISBNs { - placeholders[i] = "?" - args[i] = isbn - } - - query := fmt.Sprintf( - "SELECT isbn FROM xgy_base_item WHERE isbn IN (%s)", - strings.Join(placeholders, ","), - ) - - rows, err := db.Query(query, args...) - if err != nil { - return nil, fmt.Errorf("数据库查询失败: %w", err) - } - - // 读取存在的ISBN - for rows.Next() { - var isbn string - if err := rows.Scan(&isbn); err != nil { - rows.Close() - return nil, fmt.Errorf("扫描ISBN失败: %w", err) - } - existsMap[isbn] = true - } - rows.Close() - - if err = rows.Err(); err != nil { - return nil, fmt.Errorf("遍历数据库行时出错: %w", err) - } - - // 添加延迟避免对数据库造成压力 - if batch < totalBatches-1 { - time.Sleep(50 * time.Millisecond) - } - } - - log.Printf("数据库中存在 %d 个匹配的ISBN", len(existsMap)) - return existsMap, nil -} - -// 找出数据库中不存在的ISBN(ES中有但数据库中没有) -func findESOnlyISBNs(esISBNs []string, dbExistsMap map[string]bool) []string { - var esOnlyISBNs []string - - for _, isbn := range esISBNs { - if !dbExistsMap[isbn] { - esOnlyISBNs = append(esOnlyISBNs, isbn) - } - } - - log.Printf("ES中有 %d 个ISBN在数据库中不存在", len(esOnlyISBNs)) - return esOnlyISBNs -} - -// 导出ES独有ISBN到txt文件 -func exportESOnlyISBNs(esOnlyISBNs []string, outputFile string) error { - if len(esOnlyISBNs) == 0 { - log.Printf("没有ES独有的ISBN需要导出") - return nil - } - - // 确保输出目录存在 - outputDir := filepath.Dir(outputFile) - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("创建输出目录失败: %w", err) - } - - // 写入文件 - file, err := os.Create(outputFile) - if err != nil { - return fmt.Errorf("创建文件失败: %w", err) - } - defer file.Close() - - // 写入文件头信息 - header := fmt.Sprintf(`# ES中有但数据库中没有的ISBN列表 -# 数据库表: xgy_base_item -# ES索引: %s -# 导出时间: %s -# 记录数: %d -# 说明: 这些ISBN在ES索引中存在但在数据库表中不存在 - -`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(esOnlyISBNs)) - - if _, err := file.WriteString(header); err != nil { - return fmt.Errorf("写入文件头失败: %w", err) - } - - // 按字母顺序排序后写入 - sort.Strings(esOnlyISBNs) - successCount := 0 - for _, isbn := range esOnlyISBNs { - if _, err := file.WriteString(isbn + "\n"); err != nil { - log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) - continue - } - successCount++ - } - - log.Printf("成功导出 %d/%d 个ES独有ISBN到文件: %s", successCount, len(esOnlyISBNs), outputFile) - return nil -} - -// 主函数:查询ES中有但数据库中没有的ISBN -func mainFindESOnlyISBNs() { - // 初始化数据库连接 - dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", - "root", - "123456", - "localhost", - 3306, - "book_image") // 请根据实际情况修改数据库名 - - db, err := sql.Open("mysql", dsn) - if err != nil { - log.Fatalf("打开数据库连接失败: %v", err) - } - defer db.Close() - - // 设置连接池参数 - db.SetMaxOpenConns(20) - db.SetMaxIdleConns(10) - - // 测试数据库连接 - if err := db.Ping(); err != nil { - log.Fatalf("数据库连接测试失败: %v", err) - } - - // 初始化ES客户端 - es, err := NewESClient([]string{esAddress}, esUsername, esPassword) - if err != nil { - log.Fatalf("ES连接失败: %v", err) - } - - // 检查ES健康状态 - if err := es.CheckHealth(); err != nil { - log.Fatalf("ES健康检查失败: %v", err) - } - - startTime := time.Now() - log.Printf("开始处理ES与数据库的ISBN匹配...") - - // 步骤1: 从ES获取所有ISBN - esISBNs, err := getAllISBNsFromES(es) - if err != nil { - log.Fatalf("获取ES ISBN失败: %v", err) - } - - // 步骤2: 检查ISBN在数据库中的存在情况 - dbExistsMap, err := checkISBNsInDB(db, esISBNs) - if err != nil { - log.Fatalf("检查数据库中ISBN存在情况失败: %v", err) - } - - // 步骤3: 找出ES独有ISBN(ES中有但数据库中没有) - esOnlyISBNs := findESOnlyISBNs(esISBNs, dbExistsMap) - - // 步骤4: 导出ES独有ISBN - outputFile := "cmd/update_es_gt/missing_isbns.txt" - if err := exportESOnlyISBNs(esOnlyISBNs, outputFile); err != nil { - log.Fatalf("导出ES独有ISBN失败: %v", err) - } - - elapsed := time.Since(startTime) - - // 输出统计信息 - fmt.Printf("\n=== 处理完成 ===\n") - fmt.Printf("ES中ISBN总数: %d\n", len(esISBNs)) - fmt.Printf("数据库中匹配的ISBN数: %d\n", len(dbExistsMap)) - fmt.Printf("ES独有ISBN数(数据库中没有的): %d\n", len(esOnlyISBNs)) - fmt.Printf("独有比例: %.2f%%\n", float64(len(esOnlyISBNs))/float64(len(esISBNs))*100) - fmt.Printf("耗时: %v\n", elapsed.Round(time.Millisecond)) - fmt.Printf("输出文件: %s\n", outputFile) - - // 显示部分ES独有ISBN示例 - if len(esOnlyISBNs) > 0 { - fmt.Printf("\nES独有ISBN示例 (前10个):\n") - for i := 0; i < 10 && i < len(esOnlyISBNs); i++ { - fmt.Printf(" %s\n", esOnlyISBNs[i]) - } - if len(esOnlyISBNs) > 10 { - fmt.Printf(" ... 还有 %d 个\n", len(esOnlyISBNs)-10) - } - } -} - -// 查询并导出有销售记录且book_pic为空的ISBN -func queryAndExportSaleISBNsWithEmptyPic(es *ESClient, outputFile string) error { - log.Printf("开始查询有销售记录且book_pic为空的ISBN...") - - // 查询条件: (day_sale_7 > 0 OR day_sale_15 > 0 OR day_sale_30 > 0) AND book_pic为空 - query := map[string]interface{}{ - "query": map[string]interface{}{ - "bool": map[string]interface{}{ - "must": []map[string]interface{}{ - { - "bool": map[string]interface{}{ - "should": []map[string]interface{}{ - {"range": map[string]interface{}{"day_sale_7": map[string]interface{}{"gt": 0}}}, - {"range": map[string]interface{}{"day_sale_15": map[string]interface{}{"gt": 0}}}, - {"range": map[string]interface{}{"day_sale_30": map[string]interface{}{"gt": 0}}}, - }, - "minimum_should_match": 1, - }, - }, - { - "bool": map[string]interface{}{ - "should": []map[string]interface{}{ - // 匹配book_pic字段为空 - {"term": map[string]interface{}{"book_pic": ""}}, - // 匹配book_pic字段不存在 - { - "bool": map[string]interface{}{ - "must_not": map[string]interface{}{ - "exists": map[string]interface{}{"field": "book_pic"}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - "_source": []string{"isbn"}, - "sort": []map[string]interface{}{ - {"isbn": "asc"}, // 按ISBN排序 - }, - "size": 10000, - } - - // 打印查询条件用于验证 - queryJSON, _ := json.MarshalIndent(query, "", " ") - log.Printf("查询条件:\n%s", string(queryJSON)) - - var allISBNs []string - var searchAfter interface{} - totalCount := 0 - page := 1 - - for { - // 复制基础查询 - currentQuery := make(map[string]interface{}) - for k, v := range query { - currentQuery[k] = v - } - - // 添加游标 - if searchAfter != nil { - currentQuery["search_after"] = searchAfter - } - - body, err := json.Marshal(currentQuery) - if err != nil { - return fmt.Errorf("序列化查询失败: %w", err) - } - - log.Printf("执行第 %d 页查询...", page) - - // 执行搜索 - res, err := es.client.Search( - es.client.Search.WithIndex(esIndex), - es.client.Search.WithBody(strings.NewReader(string(body))), - es.client.Search.WithContext(context.Background()), - ) - if err != nil { - return fmt.Errorf("ES搜索失败: %w", err) - } - defer res.Body.Close() - - if res.IsError() { - bodyBytes, _ := io.ReadAll(res.Body) - return fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) - } - - // 读取并解析响应体 - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("读取响应体失败: %w", err) - } - - var result map[string]interface{} - if err := json.Unmarshal(bodyBytes, &result); err != nil { - return fmt.Errorf("解析ES响应失败: %w", err) - } - - // 检查是否有错误 - if errMsg, exists := result["error"]; exists { - return fmt.Errorf("ES返回错误: %v", errMsg) - } - - hits, ok := result["hits"].(map[string]interface{}) - if !ok { - return fmt.Errorf("无法解析hits字段") - } - - // 获取总命中数 - if totalHits, exists := hits["total"].(map[string]interface{}); exists { - if totalValue, exists := totalHits["value"]; exists { - log.Printf("ES返回总命中数: %.0f", totalValue) - } - } - - hitList, ok := hits["hits"].([]interface{}) - if !ok || len(hitList) == 0 { - log.Printf("第 %d 页没有数据,查询完成", page) - break // 没有更多数据 - } - - // 处理当前批次的数据 - batchCount := 0 - for _, hit := range hitList { - hitMap, ok := hit.(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析hit数据") - continue - } - - source, ok := hitMap["_source"].(map[string]interface{}) - if !ok { - log.Printf("警告: 无法解析_source字段") - continue - } - - isbn, ok := source["isbn"].(string) - if ok && isbn != "" { - allISBNs = append(allISBNs, isbn) - batchCount++ - } else { - log.Printf("警告: 跳过空的ISBN字段") - } - - // 更新游标(使用最后一个文档的排序值) - sortValues, ok := hitMap["sort"].([]interface{}) - if ok && len(sortValues) > 0 { - searchAfter = sortValues - } - } - - totalCount += batchCount - log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) - page++ - - // 如果返回的数量小于请求的数量,说明已经是最后一页 - if len(hitList) < 10000 { - log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) - break - } - - // 添加短暂延迟,避免对ES造成过大压力 - time.Sleep(100 * time.Millisecond) - } - - if len(allISBNs) == 0 { - return fmt.Errorf("没有找到符合条件的ISBN记录") - } - - // 去重 - isbnSet := make(map[string]bool) - uniqueISBNs := make([]string, 0) - for _, isbn := range allISBNs { - if !isbnSet[isbn] { - isbnSet[isbn] = true - uniqueISBNs = append(uniqueISBNs, isbn) - } - } - - log.Printf("去重前: %d 条, 去重后: %d 条", len(allISBNs), len(uniqueISBNs)) - - // 确保输出目录存在 - outputDir := filepath.Dir(outputFile) - if err := os.MkdirAll(outputDir, 0755); err != nil { - return fmt.Errorf("创建输出目录失败: %w", err) - } - - // 写入文件 - file, err := os.Create(outputFile) - if err != nil { - return fmt.Errorf("创建文件失败: %w", err) - } - defer file.Close() - - // 写入文件头信息 - header := fmt.Sprintf(`# 有销售记录且book_pic为空的ISBN列表 -# 查询条件: (day_sale_7 > 0 OR day_sale_15 > 0 OR day_sale_30 > 0) AND (book_pic为空 或 book_pic字段不存在) -# 索引: %s -# 查询时间: %s -# 总记录数: %d - -`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(uniqueISBNs)) - - if _, err := file.WriteString(header); err != nil { - return fmt.Errorf("写入文件头失败: %w", err) - } - - // 按字母顺序排序后写入 - sort.Strings(uniqueISBNs) - successCount := 0 - for _, isbn := range uniqueISBNs { - if _, err := file.WriteString(isbn + "\n"); err != nil { - log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) - continue - } - successCount++ - } - - log.Printf("成功导出 %d/%d 个符合条件的ISBN到文件: %s", successCount, len(uniqueISBNs), outputFile) - return nil -} - -// 查询并导出有销售记录且book_pic为空的ISBN主函数 -func mainQuerySaleISBNsWithEmptyPic() { - // 初始化ES客户端 - es, err := NewESClient([]string{esAddress}, esUsername, esPassword) - if err != nil { - log.Fatalf("ES连接失败: %v", err) - } - - // 检查ES健康状态 - if err := es.CheckHealth(); err != nil { - log.Fatalf("ES健康检查失败: %v", err) - } - - // 输出文件路径 - outputFile := "es/sale_isbns_empty_pic.txt" - - // 查询并导出ISBN - startTime := time.Now() - if err := queryAndExportSaleISBNsWithEmptyPic(es, outputFile); err != nil { - log.Fatalf("导出有销售记录且book_pic为空的ISBN失败: %v", err) - } - - elapsed := time.Since(startTime) - log.Printf("任务完成!耗时: %v,ISBN已导出到: %s", elapsed.Round(time.Millisecond), outputFile) -} +//import ( +// "context" +// "crypto/md5" +// "crypto/tls" +// "database/sql" +// "encoding/hex" +// "encoding/json" +// "fmt" +// "github.com/elastic/go-elasticsearch/v8" +// "github.com/elastic/go-elasticsearch/v8/esapi" +// "golang.org/x/image/bmp" +// "golang.org/x/image/draw" +// "golang.org/x/image/tiff" +// "golang.org/x/image/webp" +// "image" +// "image/color" +// "image/jpeg" +// "image/png" +// "io" +// "io/ioutil" +// "log" +// "mime/multipart" +// "net/http" +// "os" +// "path/filepath" +// "sort" +// "strings" +// "sync" +// "sync/atomic" +// "time" +// +// _ "github.com/go-sql-driver/mysql" +// "github.com/nfnt/resize" +//) +// +//// ES 配置 +//const ( +// esAddress = "http://103.236.91.138:9200" +// esUsername = "elastic" +// esPassword = "5mRDIUg52VC0fp14nw-F" +// esIndex = "books-from-mysql" +// ClientID = "203c5a7ba8bd4b8488d5e26f93052642" // 拼多多开放平台配置 +// ClientSecret = "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" +// PDDApiURL = "https://gw-upload.pinduoduo.com/api/upload" +//) +// +//// 配置参数 +//const ( +// maxWorkers = 15 // 最大并发worker数量 +// maxRetries = 3 // 最大重试次数 +// retryDelay = 2 * time.Second // 重试延迟 +// progressInterval = 5 * time.Second // 进度报告间隔 +//) +// +//// ES 客户端封装 +//type ESClient struct { +// client *elasticsearch.Client +//} +// +//// 数据库记录结构体 +//type CrawlerRecord struct { +// BookISBN sql.NullString +// BookPicture sql.NullString +//} +// +//// 处理结果结构体 +//type ProcessResult struct { +// Record CrawlerRecord +// Success bool +// LocalPaths []string +// PDDURLs []string +// Error error +// WorkerID int +// ProcessedAt time.Time +//} +// +//// 全局统计 +//type Statistics struct { +// Total int32 +// Success int32 +// Failed int32 +// Skipped int32 +// CurrentIndex int32 +// StartTime time.Time +//} +// +//// NewESClient 初始化 ES 客户端 +//// 说明:保持一致的连接方式(禁用证书校验、设置超时和连接池参数) +//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, +// }, +// } +// cli, err := elasticsearch.NewClient(cfg) +// if err != nil { +// return nil, err +// } +// return &ESClient{client: cli}, nil +//} +// +//// CheckHealth 检查 ES 集群健康 +//// 行为:等待状态至少为 yellow,输出基本信息 +//func (es *ESClient) CheckHealth() error { +// res, err := es.client.Cluster.Health( +// es.client.Cluster.Health.WithWaitForStatus("yellow"), +// es.client.Cluster.Health.WithTimeout(30*time.Second), +// ) +// if err != nil { +// return err +// } +// defer res.Body.Close() +// if res.IsError() { +// return fmt.Errorf("Elasticsearch 健康检查失败: %s", res.String()) +// } +// var m map[string]interface{} +// if err := json.NewDecoder(res.Body).Decode(&m); err == nil { +// log.Printf("ES status=%v nodes=%v cluster=%v", m["status"], m["number_of_nodes"], m["cluster_name"]) +// } +// return nil +//} +// +//// PDDImageProcessor 实现图片处理器 +//// pdd上传图片官方接口 +//// 上传图片到拼多多 +//func uploadToPDD(token, imagePath string) (string, error) { +// // 检查token是否有效 +// if len(token) == 0 { +// return "", fmt.Errorf("获取到的token为空") +// } +// result, err := ProcessAndUploadImage(imagePath, token) +// if err != nil { +// return "", fmt.Errorf("拼多多图片上传失败: %v", err) +// } +// +// // 解析JSON响应获取URL +// var response struct { +// RequestID string `json:"request_id"` +// URL string `json:"url"` +// } +// +// err = json.Unmarshal([]byte(result), &response) +// if err != nil { +// return "", fmt.Errorf("解析上传响应失败: %v", err) +// } +// +// if response.URL == "" { +// return "", fmt.Errorf("上传响应中未找到URL") +// } +// +// return response.URL, nil +//} +//func ProcessAndUploadImage(imagePath, token string) (string, error) { +// // 打开图片文件 +// file, err := os.Open(imagePath) +// if err != nil { +// return "", fmt.Errorf("failed to open image file: %v", err) +// } +// defer file.Close() +// +// // 准备参数 - 不包含文件路径 +// params := map[string]string{ +// "access_token": token, +// "data_type": "JSON", +// "type": "pdd.goods.img.upload", +// "client_id": ClientID, +// "timestamp": fmt.Sprintf("%d", time.Now().Unix()), +// } +// +// // 生成签名(不包含文件路径) +// params["sign"] = generateSign(params) +// +// // 创建multipart表单 +// body := &strings.Builder{} +// writer := multipart.NewWriter(body) +// +// // 写入文本参数 +// for key, value := range params { +// if err := writer.WriteField(key, value); err != nil { +// return "", fmt.Errorf("failed to write field %s: %v", key, err) +// } +// } +// +// // 写入文件流 - 使用正确的字段名 "file" +// part, err := writer.CreateFormFile("file", filepath.Base(imagePath)) +// if err != nil { +// return "", fmt.Errorf("failed to create form file: %v", err) +// } +// +// if _, err := io.Copy(part, file); err != nil { +// return "", fmt.Errorf("failed to copy file data: %v", err) +// } +// +// // 关闭writer +// if err := writer.Close(); err != nil { +// return "", fmt.Errorf("failed to close writer: %v", err) +// } +// +// // 创建请求 +// req, err := http.NewRequest("POST", PDDApiURL, strings.NewReader(body.String())) +// if err != nil { +// return "", fmt.Errorf("failed to create request: %v", err) +// } +// +// // 设置请求头 +// req.Header.Set("Content-Type", writer.FormDataContentType()) +// 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 "", fmt.Errorf("failed to send request: %v", err) +// } +// defer resp.Body.Close() +// +// // 读取响应 +// respBody, err := io.ReadAll(resp.Body) +// if err != nil { +// return "", fmt.Errorf("failed to read response: %v", err) +// } +// +// log.Printf("拼多多API响应状态: %d", resp.StatusCode) +// log.Printf("拼多多API响应内容: %s", string(respBody)) +// +// if resp.StatusCode != http.StatusOK { +// return "", fmt.Errorf("API returned error status: %d, body: %s", resp.StatusCode, string(respBody)) +// } +// +// // 解析响应 +// var result map[string]interface{} +// if err := json.Unmarshal(respBody, &result); err != nil { +// return "", fmt.Errorf("failed to parse response: %v", err) +// } +// +// // 检查API返回的错误 +// if errorResponse, exists := result["error_response"]; exists { +// errorMsg, _ := json.Marshal(errorResponse) +// return "", fmt.Errorf("API returned error: %s", string(errorMsg)) +// } +// +// // 查找成功的响应 +// for key, value := range result { +// if key != "error_response" { +// successResponse, _ := json.Marshal(value) +// return string(successResponse), nil +// } +// } +// +// return string(respBody), nil +//} +// +//// generateSign 生成拼多多API签名 +//func generateSign(params map[string]string) string { +// // 按参数名排序 +// var keys []string +// for k := range params { +// keys = append(keys, k) +// } +// sort.Strings(keys) +// // 拼接参数字符串 +// var signStr string +// for _, k := range keys { +// signStr += k + params[k] +// } +// signStr = ClientSecret + signStr + ClientSecret +// // 计算MD5并转为大写 +// hasher := md5.New() +// hasher.Write([]byte(signStr)) +// result := strings.ToUpper(hex.EncodeToString(hasher.Sum(nil))) +// return result +//} +// +//// GetPddToken 获取PDD token(简化版) +//func GetPddToken() (string, error) { +// url := "https://api.buzhiyushu.cn/huidiao/pdd/getPddChildrenBooksToken" +// +// resp, err := http.Get(url) +// if err != nil { +// return "", err +// } +// defer resp.Body.Close() +// +// body, err := ioutil.ReadAll(resp.Body) +// if err != nil { +// return "", err +// } +// +// // 使用map解析JSON +// var result map[string]interface{} +// json.Unmarshal(body, &result) +// +// // 检查业务状态 +// if code, ok := result["code"].(float64); !ok || code != 200 { +// return "", fmt.Errorf("API错误: %v", result["msg"]) +// } +// +// // 提取token +// token := result["data"].(string) +// return token, nil +//} +// +//// 并发处理记录的主要函数 +//func processRecordsConcurrently(records []CrawlerRecord, imageDir string, es *ESClient, maxWorkers int, token string) []ProcessResult { +// var stats Statistics +// stats.Total = int32(len(records)) +// stats.StartTime = time.Now() +// +// // 创建通道 +// recordChan := make(chan CrawlerRecord, len(records)) +// resultChan := make(chan ProcessResult, len(records)) +// +// // 启动worker +// var wg sync.WaitGroup +// for i := 0; i < maxWorkers; i++ { +// wg.Add(1) +// go worker(token, i, &wg, recordChan, resultChan, imageDir, es, &stats) +// } +// +// // 发送任务到通道 +// go func() { +// for _, record := range records { +// recordChan <- record +// } +// close(recordChan) +// }() +// +// // 启动进度报告 +// go progressReporter(&stats) +// +// // 收集结果 +// var results []ProcessResult +// go func() { +// for result := range resultChan { +// results = append(results, result) +// } +// }() +// +// // 等待所有worker完成 +// wg.Wait() +// close(resultChan) +// +// return results +//} +// +//// worker 处理函数 +//func worker(token string, id int, wg *sync.WaitGroup, recordChan <-chan CrawlerRecord, resultChan chan<- ProcessResult, imageDir string, es *ESClient, stats *Statistics) { +// defer wg.Done() +// +// for record := range recordChan { +// currentIndex := atomic.AddInt32(&stats.CurrentIndex, 1) +// +// result := ProcessResult{ +// Record: record, +// WorkerID: id, +// ProcessedAt: time.Now(), +// } +// +// // 检查记录有效性 +// if !isRecordValid(record) { +// atomic.AddInt32(&stats.Skipped, 1) +// result.Success = false +// result.Error = fmt.Errorf("无效记录: ISBN或图片URL为空") +// resultChan <- result +// continue +// } +// +// // 处理记录(带重试机制) +// var localPaths, pddURLs []string +// var err error +// +// for attempt := 1; attempt <= maxRetries; attempt++ { +// localPaths, pddURLs, err = processSingleRecord(token, record, imageDir, es) +// if err == nil { +// break +// } +// +// // 如果是ES记录未找到的错误,不需要重试 +// if strings.Contains(err.Error(), "ES记录未找到") { +// break +// } +// +// if attempt < maxRetries { +// log.Printf("Worker %d: 第 %d 次尝试处理 ISBN %s 失败, %d 秒后重试: %v", +// id, attempt, record.BookISBN.String, retryDelay/time.Second, err) +// time.Sleep(retryDelay) +// } +// } +// +// if err != nil { +// atomic.AddInt32(&stats.Failed, 1) +// result.Success = false +// result.Error = err +// // 即使失败,也记录已处理的本地路径(如果有) +// result.LocalPaths = localPaths +// result.PDDURLs = pddURLs +// +// // 根据错误类型记录不同的日志 +// if strings.Contains(err.Error(), "ES记录未找到") { +// log.Printf("Worker %d: ES记录未找到 [%d/%d] ISBN: %s", +// id, currentIndex, stats.Total, record.BookISBN.String) +// } else { +// log.Printf("Worker %d: 处理失败 [%d/%d] ISBN: %s, 错误: %v", +// id, currentIndex, stats.Total, record.BookISBN.String, err) +// } +// } else { +// atomic.AddInt32(&stats.Success, 1) +// result.Success = true +// result.LocalPaths = localPaths +// result.PDDURLs = pddURLs +// +// log.Printf("Worker %d: 成功处理 [%d/%d] ISBN: %s, 生成 %d 个文件, 上传 %d 个URL", +// id, currentIndex, stats.Total, record.BookISBN.String, len(localPaths), len(pddURLs)) +// } +// +// resultChan <- result +// } +//} +// +//// 检查记录有效性 +//func isRecordValid(record CrawlerRecord) bool { +// if !record.BookISBN.Valid || record.BookISBN.String == "" { +// return false +// } +// if !record.BookPicture.Valid || record.BookPicture.String == "" { +// return false +// } +// return true +//} +// +//// 进度报告器 +//func progressReporter(stats *Statistics) { +// ticker := time.NewTicker(progressInterval) +// defer ticker.Stop() +// +// for range ticker.C { +// processed := atomic.LoadInt32(&stats.CurrentIndex) +// success := atomic.LoadInt32(&stats.Success) +// failed := atomic.LoadInt32(&stats.Failed) +// skipped := atomic.LoadInt32(&stats.Skipped) +// +// elapsed := time.Since(stats.StartTime) +// rate := float64(processed) / elapsed.Seconds() +// +// // 计算预估剩余时间 +// var eta time.Duration +// if processed > 0 && rate > 0 { +// remaining := float64(stats.Total - processed) +// eta = time.Duration(remaining/rate) * time.Second +// } +// +// fmt.Printf("[进度] 已处理: %d/%d (成功: %d, 失败: %d, 跳过: %d) | 速率: %.2f 条/秒 | 运行: %v | ETA: %v\n", +// processed, stats.Total, success, failed, skipped, rate, elapsed.Round(time.Second), eta.Round(time.Second)) +// +// if processed >= stats.Total { +// break +// } +// } +//} +// +//// 打印最终统计 +//func printFinalStatistics(results []ProcessResult) { +// var success, failed, skipped int +// var totalFilesGenerated int +// var totalURLsUploaded int +// +// // 失败原因分类 +// failureReasons := make(map[string]int) +// +// for _, result := range results { +// if result.Success { +// success++ +// totalFilesGenerated += len(result.LocalPaths) +// totalURLsUploaded += len(result.PDDURLs) +// } else if result.Error != nil && strings.Contains(result.Error.Error(), "无效记录") { +// skipped++ +// failureReasons["无效记录(ISBN或URL为空)"]++ +// } else { +// failed++ +// // 即使是失败的情况,也可能生成了部分文件 +// totalFilesGenerated += len(result.LocalPaths) +// totalURLsUploaded += len(result.PDDURLs) +// +// // 分类失败原因 +// errMsg := result.Error.Error() +// switch { +// case strings.Contains(errMsg, "ES记录未找到"): +// failureReasons["ES记录未找到"]++ +// case strings.Contains(errMsg, "查询ES中ID失败"): +// failureReasons["ES查询失败"]++ +// case strings.Contains(errMsg, "下载图片失败"): +// failureReasons["图片下载失败"]++ +// case strings.Contains(errMsg, "处理图片失败"): +// failureReasons["图片处理失败"]++ +// case strings.Contains(errMsg, "上传PNG图片失败"): +// failureReasons["PNG上传失败"]++ +// case strings.Contains(errMsg, "上传JPG图片失败"): +// failureReasons["JPG上传失败"]++ +// case strings.Contains(errMsg, "更新ES数据失败"): +// failureReasons["ES更新失败"]++ +// default: +// failureReasons["其他错误"]++ +// } +// } +// } +// +// fmt.Printf("\n=== 处理完成 ===\n") +// fmt.Printf("总记录数: %d\n", len(results)) +// fmt.Printf("成功: %d\n", success) +// fmt.Printf("失败: %d\n", failed) +// fmt.Printf("跳过: %d\n", skipped) +// fmt.Printf("成功率: %.2f%%\n", float64(success)/float64(len(results))*100) +// fmt.Printf("生成文件总数: %d (平均每条记录 %.1f 个文件)\n", totalFilesGenerated, float64(totalFilesGenerated)/float64(len(results))) +// fmt.Printf("上传URL总数: %d (平均每条记录 %.1f 个URL)\n", totalURLsUploaded, float64(totalURLsUploaded)/float64(len(results))) +// +// // 显示失败原因统计 +// if len(failureReasons) > 0 { +// fmt.Printf("\n=== 失败原因统计 ===\n") +// for reason, count := range failureReasons { +// fmt.Printf(" %s: %d\n", reason, count) +// } +// } +// +// // 显示处理详情示例 +// fmt.Printf("\n=== 处理详情示例 ===\n") +// successCount := 0 +// failedCount := 0 +// for _, result := range results { +// if result.Success && successCount < 3 { +// fmt.Printf("✅ 成功: ISBN %s -> 文件: %d 个, URL: %d 个\n", +// result.Record.BookISBN.String, +// len(result.LocalPaths), +// len(result.PDDURLs)) +// successCount++ +// } else if !result.Success && failedCount < 3 && !strings.Contains(result.Error.Error(), "无效记录") { +// fmt.Printf("❌ 失败: ISBN %s -> 错误: %v\n", +// result.Record.BookISBN.String, +// result.Error) +// failedCount++ +// } +// if successCount >= 3 && failedCount >= 3 { +// break +// } +// } +//} +// +//// 处理单条记录 +//func processSingleRecord(token string, record CrawlerRecord, imageDir string, es *ESClient) ([]string, []string, error) { +// // 更新ES +// ids, err := es.FindIDsByISBN(esIndex, record.BookISBN.String) +// if err != nil { +// return nil, nil, fmt.Errorf("查询ES中ID失败: %v", err) +// } +// var pngImageUrl string +// var jpgImageUrl string +// var localPaths []string +// var pddURLs []string +// if ids != "" { +// // 下载并处理图片 +// pngPath, jpgPath, err := processAndSaveImage(record, imageDir) +// if err != nil { +// err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) +// if err != nil { +// log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) +// } else { +// log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) +// } +// return nil, nil, fmt.Errorf("处理图片失败: %v", err) +// } +// localPaths = []string{pngPath, jpgPath} +// // 上传到PDD +// pngImageUrl, err = uploadToPDD(token, pngPath) +// if err != nil { +// err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) +// if err != nil { +// log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) +// } else { +// log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) +// } +// return nil, nil, fmt.Errorf("上传PNG图片失败: %v", err) +// } +// // 上传到PDD +// jpgImageUrl, err = uploadToPDD(token, jpgPath) +// if err != nil { +// err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) +// if err != nil { +// log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) +// } else { +// log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) +// } +// return nil, nil, fmt.Errorf("上传JPG图片失败: %v", err) +// } +// pddURLs = []string{pngImageUrl, jpgImageUrl} +// err = es.UpdateBookPicsByID(esIndex, ids, "", pngImageUrl, jpgImageUrl) +// if err != nil { +// err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) +// if err != nil { +// log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) +// } else { +// log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) +// } +// return nil, nil, fmt.Errorf("更新ES数据失败: %v", err) +// } +// +// for _, path := range localPaths { +// // ES更新成功后删除本地图片 +// if removeErr := os.Remove(path); removeErr == nil { +// log.Printf("ES更新成功,已删除本地图片: %s", path) +// } else { +// log.Printf("警告: 无法删除本地图片 %s: %v", path, removeErr) +// } +// } +// } else { +// // ids为空,将ISBN存储到txt文件 +// err := saveISBNToFile(record.BookISBN.String, record.BookPicture.String) +// if err != nil { +// log.Printf("警告: 无法保存未找到的ISBN到文件: %v", err) +// } else { +// log.Printf("未找到ISBN %s 对应的ES记录,已保存到文件", record.BookISBN.String) +// } +// return nil, nil, fmt.Errorf("未找到ISBN %s 对应的ES记录", record.BookISBN.String) +// } +// return localPaths, pddURLs, nil +//} +// +//// 保存未找到的ISBN和图片URL到txt文件(CSV格式,带去重) +//func saveISBNToFile(isbn string, imageUrl string) error { +// filename := "cmd/update_es_gt/xgy_not_found_isbns.txt" +// +// // 读取现有内容检查是否已存在 +// existingRecords := make(map[string]bool) +// if content, err := os.ReadFile(filename); err == nil { +// lines := strings.Split(string(content), "\n") +// for _, line := range lines { +// if line != "" && !strings.HasPrefix(line, "#") { +// parts := strings.Split(line, ",") +// if len(parts) > 0 { +// existingRecords[parts[0]] = true // 以ISBN作为去重依据 +// } +// } +// } +// } +// +// // 如果已存在,则不重复添加 +// if existingRecords[isbn] { +// return nil +// } +// // 以追加模式打开文件 +// file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) +// if err != nil { +// return fmt.Errorf("打开文件失败: %v", err) +// } +// defer file.Close() +// // 如果是空文件,先写入CSV表头 +// stat, err := file.Stat() +// if err == nil && stat.Size() == 0 { +// header := "# ISBN,ImageURL\n" +// if _, err := file.WriteString(header); err != nil { +// return fmt.Errorf("写入表头失败: %v", err) +// } +// } +// +// // 写入ISBN和图片URL,用逗号分隔,并添加换行符 +// line := fmt.Sprintf("%s,%s\n", isbn, imageUrl) +// _, err = file.WriteString(line) +// if err != nil { +// return fmt.Errorf("写入文件失败: %v", err) +// } +// +// return nil +//} +// +//// 下载并处理图片 +//func processAndSaveImage(record CrawlerRecord, saveDir string) (string, string, error) { +// // 下载图片 +// img, originalFormat, err := downloadImage(record.BookPicture.String) +// if err != nil { +// return "", "", fmt.Errorf("下载图片失败: %v", err) +// } +// +// fmt.Printf("下载成功,原始格式: %s\n", originalFormat) +// +// // 调整图片高度为600,等比例缩放 +// //resizedImg := resizeImageToHeight(img, 600) +// // 使用高质量缩放调整图片高度为600,等比例缩放 +// resizedImg := resizeToHeightHighQuality(img, 600) +// fmt.Printf("缩放后尺寸: %dx%d\n", resizedImg.Bounds().Dx(), resizedImg.Bounds().Dy()) +// +// // 创建800x800的透明背景 +// finalImg := createCenteredImage(resizedImg, 800, 800, true) +// +// // 创建800x800的白色背景(用于JPG) +// whiteImg := createCenteredImage(resizedImg, 800, 800, false) +// +// // 生成文件名 +// filename := fmt.Sprintf("%s", record.BookISBN.String) +// // 清理文件名中的非法字符 +// filename = sanitizeFilename(filename) +// +// // PNG文件路径 +// pngPath := filepath.Join(saveDir, filename+".png") +// // JPG文件路径 +// jpgPath := filepath.Join(saveDir, filename+".jpg") +// +// // 保存为PNG图片 +// err = savePNG(finalImg, pngPath) +// if err != nil { +// return "", "", fmt.Errorf("保存图片失败: %v", err) +// } +// +// // 保存为JPG图片(白色背景) +// err = saveJPG(whiteImg, jpgPath, 95) // 95%质量 +// if err != nil { +// return "", "", fmt.Errorf("保存JPG图片失败: %v", err) +// } +// +// fmt.Printf("转换成功: %s -> %s, 保存路径: %s\n", originalFormat, "PNG", pngPath) +// fmt.Printf("转换成功: %s -> %s, 保存路径: %s\n", originalFormat, "JPG", jpgPath) +// return pngPath, jpgPath, nil +//} +// +//// 下载图片 +//func downloadImage(url string) (image.Image, string, error) { +// // 创建HTTP客户端,设置超时等参数 +// client := &http.Client{ +// Timeout: 30 * time.Second, +// } +// +// resp, err := client.Get(url) +// if err != nil { +// return nil, "", err +// } +// defer resp.Body.Close() +// +// if resp.StatusCode != http.StatusOK { +// return nil, "", fmt.Errorf("HTTP请求失败,状态码: %d", resp.StatusCode) +// } +// +// // 读取响应体前几个字节来判断图片格式 +// peekBytes := make([]byte, 512) +// n, err := resp.Body.Read(peekBytes) +// if err != nil && err != io.EOF { +// return nil, "", err +// } +// +// // 创建一个新的Reader,包含已读取的数据和剩余数据 +// reader := io.MultiReader(strings.NewReader(string(peekBytes[:n])), resp.Body) +// +// // 根据文件头识别图片格式 +// contentType := http.DetectContentType(peekBytes[:n]) +// fmt.Printf("检测到的Content-Type: %s\n", contentType) +// +// var img image.Image +// var format string +// +// // 根据Content-Type或文件扩展名选择解码器 +// switch { +// case strings.Contains(contentType, "jpeg") || strings.HasSuffix(strings.ToLower(url), ".jpg") || strings.HasSuffix(strings.ToLower(url), ".jpeg"): +// img, err = jpeg.Decode(reader) +// format = "JPEG" +// case strings.Contains(contentType, "png") || strings.HasSuffix(strings.ToLower(url), ".png"): +// img, err = png.Decode(reader) +// format = "PNG" +// case strings.Contains(contentType, "webp") || strings.HasSuffix(strings.ToLower(url), ".webp"): +// img, err = webp.Decode(reader) +// format = "WEBP" +// case strings.Contains(contentType, "bmp") || strings.HasSuffix(strings.ToLower(url), ".bmp"): +// img, err = bmp.Decode(reader) +// format = "BMP" +// case strings.Contains(contentType, "tiff") || strings.HasSuffix(strings.ToLower(url), ".tiff") || strings.HasSuffix(strings.ToLower(url), ".tif"): +// img, err = tiff.Decode(reader) +// format = "TIFF" +// default: +// // 尝试通用解码 +// img, format, err = image.Decode(reader) +// if err != nil { +// return nil, "", fmt.Errorf("不支持的图片格式: %s, 错误: %v", contentType, err) +// } +// } +// +// if err != nil { +// return nil, "", fmt.Errorf("解码图片失败: %v", err) +// } +// +// return img, format, nil +//} +// +//// 高质量等比例缩放到指定高度 +//func resizeToHeightHighQuality(src image.Image, targetHeight int) image.Image { +// bounds := src.Bounds() +// srcWidth := bounds.Dx() +// srcHeight := bounds.Dy() +// +// // 如果原图高度已经小于等于目标高度,且宽度合适,可以直接返回 +// //if srcHeight <= targetHeight { +// // return src +// //} +// +// // 计算等比例缩放后的宽度 +// targetWidth := uint(float64(srcWidth) * float64(targetHeight) / float64(srcHeight)) +// +// // 使用 Lanczos3 插值算法进行高质量缩放 +// return resize.Resize(targetWidth, uint(targetHeight), src, resize.Lanczos3) +//} +// +//// 创建居中图片(将原图放在指定大小的透明背景中央) +//func createCenteredImage(src image.Image, width, height int, transparent bool) *image.RGBA { +// // 创建透明背景 +// dst := image.NewRGBA(image.Rect(0, 0, width, height)) +// +// // 设置背景颜色 +// var bgColor color.Color +// if transparent { +// bgColor = color.RGBA{0, 0, 0, 0} // 透明 +// } else { +// bgColor = color.RGBA{255, 255, 255, 255} // 白色 +// } +// +// // 填充透明背景 +// //transparent := color.RGBA{0, 0, 0, 0} +// draw.Draw(dst, dst.Bounds(), &image.Uniform{bgColor}, image.Point{}, draw.Src) +// +// // 计算居中位置 +// srcBounds := src.Bounds() +// srcWidth := srcBounds.Dx() +// srcHeight := srcBounds.Dy() +// +// x := (width - srcWidth) / 2 +// y := (height - srcHeight) / 2 +// +// // 将原图绘制到中央 +// draw.Draw(dst, image.Rect(x, y, x+srcWidth, y+srcHeight), src, image.Point{}, draw.Over) +// +// return dst +//} +// +//// 保存为PNG图片 +//func savePNG(img image.Image, filename string) error { +// file, err := os.Create(filename) +// if err != nil { +// return err +// } +// defer file.Close() +// +// return png.Encode(file, img) +//} +// +//// 保存为JPG图片 +//func saveJPG(img image.Image, filename string, quality int) error { +// file, err := os.Create(filename) +// if err != nil { +// return err +// } +// defer file.Close() +// +// // 设置JPEG编码选项 +// options := &jpeg.Options{ +// Quality: quality, // 1-100,越高质量越好 +// } +// +// return jpeg.Encode(file, img, options) +//} +// +//// 清理文件名中的非法字符 +//func sanitizeFilename(filename string) string { +// // 替换Windows文件名中不允许的字符 +// invalidChars := []string{"\\", "/", ":", "*", "?", "\"", "<", ">", "|"} +// for _, char := range invalidChars { +// filename = strings.ReplaceAll(filename, char, "_") +// } +// // 移除或替换其他可能的问题字符 +// filename = strings.TrimSpace(filename) +// if filename == "" { +// filename = "unknown" +// } +// return filename +//} +// +//// 从数据库获取记录 +//func getRecords(db *sql.DB) ([]CrawlerRecord, error) { +// // 查询所有记录,包括 NULL 值 +// query := "SELECT book_isbn, book_picture FROM dk_crawler_record_info" +// rows, err := db.Query(query) +// if err != nil { +// return nil, err +// } +// defer rows.Close() +// +// var records []CrawlerRecord +// for rows.Next() { +// var record CrawlerRecord +// // 使用 sql.NullString 来接收可能为 NULL 的字段 +// err := rows.Scan(&record.BookISBN, &record.BookPicture) +// if err != nil { +// fmt.Printf("扫描记录失败: %v\n", err) +// continue +// } +// records = append(records, record) +// } +// +// // 检查遍历过程中是否有错误 +// if err = rows.Err(); err != nil { +// return nil, err +// } +// +// return records, nil +//} +// +//// FindIDsByISBN 根据 ISBN 查询文档 ID 列表 +//func (es *ESClient) FindIDsByISBN(index, isbn string) (string, error) { +// q := map[string]interface{}{ +// "query": map[string]interface{}{ +// "term": map[string]interface{}{"isbn": isbn}, +// }, +// "_source": false, +// "size": 1000, +// } +// b, _ := json.Marshal(q) +// res, err := es.client.Search( +// es.client.Search.WithIndex(index), +// es.client.Search.WithBody(strings.NewReader(string(b))), +// es.client.Search.WithContext(context.Background()), +// ) +// if err != nil { +// return "", err +// } +// defer res.Body.Close() +// if res.IsError() { +// return "", fmt.Errorf("搜索失败: %s", res.String()) +// } +// var r map[string]interface{} +// if err := json.NewDecoder(res.Body).Decode(&r); err != nil { +// return "", err +// } +// hits, _ := r["hits"].(map[string]interface{}) +// arr, _ := hits["hits"].([]interface{}) +// var ids string +// for _, h := range arr { +// m, _ := h.(map[string]interface{}) +// id, _ := m["_id"].(string) +// if id != "" { +// //ids = append(ids, id) +// ids = id +// } +// } +// return ids, nil +//} +// +//func (es *ESClient) UpdateBookPicsByID(index, id, localImageS, pngImageUrl, jpgImageUrl string) error { +// bookPicJSON, err := json.Marshal(map[string]string{ +// "localPath": localImageS, +// "pddPath": jpgImageUrl, +// }) +// if err != nil { +// return fmt.Errorf("序列化 book_pic_w 失败: %w", err) +// } +// +// bookPicBJSON, err := json.Marshal(map[string]string{ +// "localPath": localImageS, +// "pddResponse": pngImageUrl, +// }) +// if err != nil { +// return fmt.Errorf("序列化 book_pic_b 失败: %w", err) +// } +// // 构建更新文档 +// payload := map[string]interface{}{ +// "doc": map[string]string{ +// "book_pic": string(bookPicJSON), +// "book_pic_b": string(bookPicBJSON), +// }, +// } +// // JSON 序列化整个更新请求 +// body, err := json.Marshal(payload) +// if err != nil { +// return fmt.Errorf("序列化更新请求失败: %w", err) +// } +// req := esapi.UpdateRequest{ +// Index: index, +// DocumentID: id, +// Body: strings.NewReader(string(body)), +// } +// res, err := req.Do(context.Background(), es.client) +// if err != nil { +// return err +// } +// defer res.Body.Close() +// if res.IsError() { +// data, _ := io.ReadAll(res.Body) +// return fmt.Errorf("ES 更新失败: %s", data) +// } +// return nil +//} +// +//// 从 sql.NullString 获取字符串值 +//func getStringValue(nullString sql.NullString) string { +// if nullString.Valid { +// return nullString.String +// } +// return "NULL" +//} +// +//func main() { +// //// 获取token +// //token, err := GetPddToken() +// //if err != nil { +// // fmt.Errorf("获取拼多多token失败: %v", err) +// //} +// //fmt.Println("token=", token) +// //// 数据源名称格式 +// //dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", +// // "root", +// // "123456", +// // "localhost", +// // 3306, +// // "book_image") +// //db, err := sql.Open("mysql", dsn) +// //if err != nil { +// // fmt.Printf("打开数据库连接失败: %v", err) +// //} +// //// 设置连接池参数 +// //db.SetMaxOpenConns(20) // 最大打开连接数 +// //db.SetMaxIdleConns(10) +// //err = db.Ping() +// //if err != nil { +// // fmt.Printf("数据库连接测试失败: %v", err) +// //} +// // +// //// 查询数据 +// //records, err := getRecords(db) +// //if err != nil { +// // fmt.Printf("查询失败: %v", err) +// //} +// //imageDir := "D:\\image" +// //err = os.MkdirAll(imageDir, 0755) +// //if err != nil { +// // fmt.Sprintf("创建目录失败: %v", err) +// //} +// //fmt.Printf("找到 %d 条记录需要处理\n", len(records)) +// // +// //es, err := NewESClient([]string{esAddress}, esUsername, esPassword) +// //if err != nil { +// // log.Fatalf("ES 连接失败: %v", err) +// //} +// //if err := es.CheckHealth(); err != nil { +// // log.Fatalf("ES 健康检查失败: %v", err) +// //} +// //// 启动并发处理 +// //results := processRecordsConcurrently(records, imageDir, es, maxWorkers, token) +// // +// //// 输出最终统计 +// //printFinalStatistics(results) +// +// //mainQuerySaleISBNs() +// //mainFindESOnlyISBNs() +// mainQuerySaleISBNsWithEmptyPic() +//} +// +//// 查询并导出有销售记录且book_pic字符串中pddPath为空的ISBN +//func queryAndExportSaleISBNs(es *ESClient, outputFile string) error { +// log.Printf("开始查询有销售记录且book_pic字符串中pddPath为空的ISBN...") +// +// // 使用其他字段排序,比如 isbn 字段或者时间字段 +// query := map[string]interface{}{ +// "query": map[string]interface{}{ +// "bool": map[string]interface{}{ +// "must": []map[string]interface{}{ +// { +// "bool": map[string]interface{}{ +// "should": []map[string]interface{}{ +// {"range": map[string]interface{}{"day_sale_7": map[string]interface{}{"gt": 0}}}, +// {"range": map[string]interface{}{"day_sale_15": map[string]interface{}{"gt": 0}}}, +// {"range": map[string]interface{}{"day_sale_30": map[string]interface{}{"gt": 0}}}, +// {"range": map[string]interface{}{"day_sale_60": map[string]interface{}{"gt": 0}}}, +// }, +// "minimum_should_match": 1, +// }, +// }, +// { +// "bool": map[string]interface{}{ +// "should": []map[string]interface{}{ +// // 匹配 pddPath:"" 的JSON字符串 +// {"regexp": map[string]interface{}{"book_pic": ".*\"pddPath\":\"\".*"}}, +// // 匹配 pddPath: "" (带空格的) +// {"regexp": map[string]interface{}{"book_pic": ".*\"pddPath\":\\s*\"\".*"}}, +// // 匹配整个book_pic字段为空 +// {"term": map[string]interface{}{"book_pic": ""}}, +// // 匹配book_pic字段不存在 +// { +// "bool": map[string]interface{}{ +// "must_not": map[string]interface{}{ +// "exists": map[string]interface{}{"field": "book_pic"}, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// "_source": []string{"isbn"}, +// "sort": []map[string]interface{}{ +// {"isbn": "asc"}, // 使用 isbn 字段排序,或者使用其他可排序字段 +// }, +// "size": 10000, +// } +// +// // 打印查询条件用于验证 +// queryJSON, _ := json.MarshalIndent(query, "", " ") +// log.Printf("查询条件:\n%s", string(queryJSON)) +// +// var allISBNs []string +// var searchAfter interface{} +// totalCount := 0 +// page := 1 +// +// for { +// // 复制基础查询 +// currentQuery := make(map[string]interface{}) +// for k, v := range query { +// currentQuery[k] = v +// } +// +// // 添加游标 +// if searchAfter != nil { +// currentQuery["search_after"] = searchAfter +// } +// +// body, err := json.Marshal(currentQuery) +// if err != nil { +// return fmt.Errorf("序列化查询失败: %w", err) +// } +// +// log.Printf("执行第 %d 页查询...", page) +// +// // 执行搜索 +// res, err := es.client.Search( +// es.client.Search.WithIndex(esIndex), +// es.client.Search.WithBody(strings.NewReader(string(body))), +// es.client.Search.WithContext(context.Background()), +// ) +// if err != nil { +// return fmt.Errorf("ES搜索失败: %w", err) +// } +// defer res.Body.Close() +// +// if res.IsError() { +// bodyBytes, _ := io.ReadAll(res.Body) +// return fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) +// } +// +// // 读取并解析响应体 +// bodyBytes, err := io.ReadAll(res.Body) +// if err != nil { +// return fmt.Errorf("读取响应体失败: %w", err) +// } +// +// var result map[string]interface{} +// if err := json.Unmarshal(bodyBytes, &result); err != nil { +// return fmt.Errorf("解析ES响应失败: %w", err) +// } +// +// // 检查是否有错误 +// if errMsg, exists := result["error"]; exists { +// return fmt.Errorf("ES返回错误: %v", errMsg) +// } +// +// hits, ok := result["hits"].(map[string]interface{}) +// if !ok { +// return fmt.Errorf("无法解析hits字段") +// } +// +// // 获取总命中数 +// if totalHits, exists := hits["total"].(map[string]interface{}); exists { +// if totalValue, exists := totalHits["value"]; exists { +// log.Printf("ES返回总命中数: %.0f", totalValue) +// } +// } +// +// hitList, ok := hits["hits"].([]interface{}) +// if !ok || len(hitList) == 0 { +// log.Printf("第 %d 页没有数据,查询完成", page) +// break // 没有更多数据 +// } +// +// // 处理当前批次的数据 +// batchCount := 0 +// for _, hit := range hitList { +// hitMap, ok := hit.(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析hit数据") +// continue +// } +// +// source, ok := hitMap["_source"].(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析_source字段") +// continue +// } +// +// isbn, ok := source["isbn"].(string) +// if ok && isbn != "" { +// allISBNs = append(allISBNs, isbn) +// batchCount++ +// } else { +// log.Printf("警告: 跳过空的ISBN字段") +// } +// +// // 更新游标(使用最后一个文档的排序值) +// sortValues, ok := hitMap["sort"].([]interface{}) +// if ok && len(sortValues) > 0 { +// searchAfter = sortValues +// } +// } +// +// totalCount += batchCount +// log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) +// page++ +// +// // 如果返回的数量小于请求的数量,说明已经是最后一页 +// if len(hitList) < 10000 { +// log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) +// break +// } +// +// // 添加短暂延迟,避免对ES造成过大压力 +// time.Sleep(100 * time.Millisecond) +// } +// +// if len(allISBNs) == 0 { +// return fmt.Errorf("没有找到符合条件的ISBN记录") +// } +// +// // 去重 +// isbnSet := make(map[string]bool) +// uniqueISBNs := make([]string, 0) +// for _, isbn := range allISBNs { +// if !isbnSet[isbn] { +// isbnSet[isbn] = true +// uniqueISBNs = append(uniqueISBNs, isbn) +// } +// } +// +// log.Printf("去重前: %d 条, 去重后: %d 条", len(allISBNs), len(uniqueISBNs)) +// +// // 确保输出目录存在 +// outputDir := filepath.Dir(outputFile) +// if err := os.MkdirAll(outputDir, 0755); err != nil { +// return fmt.Errorf("创建输出目录失败: %w", err) +// } +// +// // 写入文件 +// file, err := os.Create(outputFile) +// if err != nil { +// return fmt.Errorf("创建文件失败: %w", err) +// } +// defer file.Close() +// +// // 写入文件头信息 +// header := fmt.Sprintf(`# 有销售记录且book_pic字符串中pddPath为空的ISBN列表 +//# 查询条件: (day_sale_7 > 0 OR day_sale_15 > 0 OR day_sale_30 > 0 OR day_sale_60 > 0) AND (book_pic包含"pddPath":"" 或 book_pic为空 或 book_pic字段不存在) +//# 索引: %s +//# 统计时间: %s +//# 总记录数: %d +// +//`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(uniqueISBNs)) +// +// if _, err := file.WriteString(header); err != nil { +// return fmt.Errorf("写入文件头失败: %w", err) +// } +// +// // 按字母顺序排序后写入 +// sort.Strings(uniqueISBNs) +// successCount := 0 +// for _, isbn := range uniqueISBNs { +// if _, err := file.WriteString(isbn + "\n"); err != nil { +// log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) +// continue +// } +// successCount++ +// } +// +// log.Printf("成功导出 %d/%d 个有销售记录且book_pic字符串中pddPath为空的ISBN到文件: %s", successCount, len(uniqueISBNs), outputFile) +// return nil +//} +// +//// 查询并导出销售ISBN的主函数 +//func mainQuerySaleISBNs() { +// // 初始化ES客户端 +// es, err := NewESClient([]string{esAddress}, esUsername, esPassword) +// if err != nil { +// log.Fatalf("ES连接失败: %v", err) +// } +// +// // 检查ES健康状态 +// if err := es.CheckHealth(); err != nil { +// log.Fatalf("ES健康检查失败: %v", err) +// } +// +// // 输出文件路径 +// outputFile := "cmd/update_es_gt/all_isbns.txt" +// +// // 查询并导出ISBN +// startTime := time.Now() +// if err := exportAllISBNs(es, outputFile); err != nil { +// log.Fatalf("导出销售ISBN失败: %v", err) +// } +// +// elapsed := time.Since(startTime) +// log.Printf("任务完成!耗时: %v,ISBN已导出到: %s", elapsed.Round(time.Millisecond), outputFile) +//} +// +//// 导出所有ISBN到txt文件 +//func exportAllISBNs(es *ESClient, outputFile string) error { +// log.Printf("开始导出所有ISBN...") +// +// // 查询所有包含isbn字段的文档 +// query := map[string]interface{}{ +// "query": map[string]interface{}{ +// "exists": map[string]interface{}{ +// "field": "isbn", +// }, +// }, +// "_source": []string{"isbn"}, +// "sort": []map[string]interface{}{ +// {"isbn": "asc"}, // 按ISBN排序 +// }, +// "size": 10000, +// } +// +// var allISBNs []string +// var searchAfter interface{} +// totalCount := 0 +// page := 1 +// +// for { +// // 复制基础查询 +// currentQuery := make(map[string]interface{}) +// for k, v := range query { +// currentQuery[k] = v +// } +// +// // 添加游标 +// if searchAfter != nil { +// currentQuery["search_after"] = searchAfter +// } +// +// body, err := json.Marshal(currentQuery) +// if err != nil { +// return fmt.Errorf("序列化查询失败: %w", err) +// } +// +// log.Printf("执行第 %d 页查询...", page) +// +// // 执行搜索 +// res, err := es.client.Search( +// es.client.Search.WithIndex(esIndex), +// es.client.Search.WithBody(strings.NewReader(string(body))), +// es.client.Search.WithContext(context.Background()), +// ) +// if err != nil { +// return fmt.Errorf("ES搜索失败: %w", err) +// } +// defer res.Body.Close() +// +// if res.IsError() { +// bodyBytes, _ := io.ReadAll(res.Body) +// return fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) +// } +// +// // 读取并解析响应体 +// bodyBytes, err := io.ReadAll(res.Body) +// if err != nil { +// return fmt.Errorf("读取响应体失败: %w", err) +// } +// +// var result map[string]interface{} +// if err := json.Unmarshal(bodyBytes, &result); err != nil { +// return fmt.Errorf("解析ES响应失败: %w", err) +// } +// +// // 检查是否有错误 +// if errMsg, exists := result["error"]; exists { +// return fmt.Errorf("ES返回错误: %v", errMsg) +// } +// +// hits, ok := result["hits"].(map[string]interface{}) +// if !ok { +// return fmt.Errorf("无法解析hits字段") +// } +// +// // 获取总命中数 +// if totalHits, exists := hits["total"].(map[string]interface{}); exists { +// if totalValue, exists := totalHits["value"]; exists { +// if page == 1 { +// log.Printf("ES索引中共有 %.0f 条包含ISBN的记录", totalValue) +// } +// } +// } +// +// hitList, ok := hits["hits"].([]interface{}) +// if !ok || len(hitList) == 0 { +// log.Printf("第 %d 页没有数据,查询完成", page) +// break // 没有更多数据 +// } +// +// // 处理当前批次的数据 +// batchCount := 0 +// for _, hit := range hitList { +// hitMap, ok := hit.(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析hit数据") +// continue +// } +// +// source, ok := hitMap["_source"].(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析_source字段") +// continue +// } +// +// isbn, ok := source["isbn"].(string) +// if ok && isbn != "" { +// allISBNs = append(allISBNs, isbn) +// batchCount++ +// } else { +// log.Printf("警告: 跳过空的ISBN字段") +// } +// +// // 更新游标(使用最后一个文档的排序值) +// sortValues, ok := hitMap["sort"].([]interface{}) +// if ok && len(sortValues) > 0 { +// searchAfter = sortValues +// } +// } +// +// totalCount += batchCount +// log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) +// page++ +// +// // 如果返回的数量小于请求的数量,说明已经是最后一页 +// if len(hitList) < 10000 { +// log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) +// break +// } +// +// // 添加短暂延迟,避免对ES造成过大压力 +// time.Sleep(100 * time.Millisecond) +// } +// +// if len(allISBNs) == 0 { +// return fmt.Errorf("没有找到包含ISBN字段的记录") +// } +// +// // 去重 +// isbnSet := make(map[string]bool) +// uniqueISBNs := make([]string, 0) +// for _, isbn := range allISBNs { +// if !isbnSet[isbn] { +// isbnSet[isbn] = true +// uniqueISBNs = append(uniqueISBNs, isbn) +// } +// } +// +// log.Printf("去重前: %d 条, 去重后: %d 条", len(allISBNs), len(uniqueISBNs)) +// +// // 确保输出目录存在 +// outputDir := filepath.Dir(outputFile) +// if err := os.MkdirAll(outputDir, 0755); err != nil { +// return fmt.Errorf("创建输出目录失败: %w", err) +// } +// +// // 写入文件 +// file, err := os.Create(outputFile) +// if err != nil { +// return fmt.Errorf("创建文件失败: %w", err) +// } +// defer file.Close() +// +// // 写入文件头信息 +// header := fmt.Sprintf(`# 所有ISBN列表 +//# 索引: %s +//# 导出时间: %s +//# 总记录数: %d +// +//`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(uniqueISBNs)) +// +// if _, err := file.WriteString(header); err != nil { +// return fmt.Errorf("写入文件头失败: %w", err) +// } +// +// // 按字母顺序排序后写入 +// sort.Strings(uniqueISBNs) +// successCount := 0 +// for _, isbn := range uniqueISBNs { +// if _, err := file.WriteString(isbn + "\n"); err != nil { +// log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) +// continue +// } +// successCount++ +// } +// +// log.Printf("成功导出 %d/%d 个ISBN到文件: %s", successCount, len(uniqueISBNs), outputFile) +// return nil +//} +// +//// 从ES获取所有ISBN +//func getAllISBNsFromES(es *ESClient) ([]string, error) { +// log.Printf("开始从ES索引 %s 获取所有ISBN...", esIndex) +// +// var allISBNs []string +// var searchAfter interface{} +// totalCount := 0 +// page := 1 +// +// for { +// query := map[string]interface{}{ +// "query": map[string]interface{}{ +// "exists": map[string]interface{}{ +// "field": "isbn", +// }, +// }, +// "_source": []string{"isbn"}, +// "sort": []map[string]interface{}{ +// {"isbn": "asc"}, +// }, +// "size": 10000, +// } +// +// // 添加游标 +// if searchAfter != nil { +// query["search_after"] = searchAfter +// } +// +// body, err := json.Marshal(query) +// if err != nil { +// return nil, fmt.Errorf("序列化查询失败: %w", err) +// } +// +// log.Printf("执行第 %d 页ES查询...", page) +// +// // 执行搜索 +// res, err := es.client.Search( +// es.client.Search.WithIndex(esIndex), +// es.client.Search.WithBody(strings.NewReader(string(body))), +// es.client.Search.WithContext(context.Background()), +// ) +// if err != nil { +// return nil, fmt.Errorf("ES搜索失败: %w", err) +// } +// defer res.Body.Close() +// +// if res.IsError() { +// bodyBytes, _ := io.ReadAll(res.Body) +// return nil, fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) +// } +// +// // 解析响应 +// var result map[string]interface{} +// if err := json.NewDecoder(res.Body).Decode(&result); err != nil { +// return nil, fmt.Errorf("解析ES响应失败: %w", err) +// } +// +// hits, ok := result["hits"].(map[string]interface{}) +// if !ok { +// return nil, fmt.Errorf("无法解析hits字段") +// } +// +// hitList, ok := hits["hits"].([]interface{}) +// if !ok || len(hitList) == 0 { +// log.Printf("第 %d 页没有数据,查询完成", page) +// break +// } +// +// // 处理当前批次的数据 +// batchCount := 0 +// for _, hit := range hitList { +// hitMap, ok := hit.(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析hit数据") +// continue +// } +// +// source, ok := hitMap["_source"].(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析_source字段") +// continue +// } +// +// isbn, ok := source["isbn"].(string) +// if ok && isbn != "" { +// allISBNs = append(allISBNs, isbn) +// batchCount++ +// } +// +// // 更新游标 +// sortValues, ok := hitMap["sort"].([]interface{}) +// if ok && len(sortValues) > 0 { +// searchAfter = sortValues +// } +// } +// +// totalCount += batchCount +// log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) +// page++ +// +// // 如果返回的数量小于请求的数量,说明已经是最后一页 +// if len(hitList) < 10000 { +// log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) +// break +// } +// +// time.Sleep(100 * time.Millisecond) +// } +// +// if len(allISBNs) == 0 { +// return nil, fmt.Errorf("ES中没有找到包含ISBN字段的记录") +// } +// +// log.Printf("从ES中获取到 %d 个ISBN", len(allISBNs)) +// return allISBNs, nil +//} +// +//// 批量检查ISBN在数据库中是否存在 +//func checkISBNsInDB(db *sql.DB, isbns []string) (map[string]bool, error) { +// log.Printf("开始检查 %d 个ISBN在数据库中的存在情况...", len(isbns)) +// +// existsMap := make(map[string]bool) +// +// // 分批处理,避免SQL语句过长 +// batchSize := 1000 +// totalBatches := (len(isbns) + batchSize - 1) / batchSize +// +// for batch := 0; batch < totalBatches; batch++ { +// start := batch * batchSize +// end := start + batchSize +// if end > len(isbns) { +// end = len(isbns) +// } +// +// batchISBNs := isbns[start:end] +// log.Printf("处理数据库批次 %d/%d: ISBN范围 %d-%d", batch+1, totalBatches, start+1, end) +// +// // 构建IN查询的占位符 +// placeholders := make([]string, len(batchISBNs)) +// args := make([]interface{}, len(batchISBNs)) +// for i, isbn := range batchISBNs { +// placeholders[i] = "?" +// args[i] = isbn +// } +// +// query := fmt.Sprintf( +// "SELECT isbn FROM xgy_base_item WHERE isbn IN (%s)", +// strings.Join(placeholders, ","), +// ) +// +// rows, err := db.Query(query, args...) +// if err != nil { +// return nil, fmt.Errorf("数据库查询失败: %w", err) +// } +// +// // 读取存在的ISBN +// for rows.Next() { +// var isbn string +// if err := rows.Scan(&isbn); err != nil { +// rows.Close() +// return nil, fmt.Errorf("扫描ISBN失败: %w", err) +// } +// existsMap[isbn] = true +// } +// rows.Close() +// +// if err = rows.Err(); err != nil { +// return nil, fmt.Errorf("遍历数据库行时出错: %w", err) +// } +// +// // 添加延迟避免对数据库造成压力 +// if batch < totalBatches-1 { +// time.Sleep(50 * time.Millisecond) +// } +// } +// +// log.Printf("数据库中存在 %d 个匹配的ISBN", len(existsMap)) +// return existsMap, nil +//} +// +//// 找出数据库中不存在的ISBN(ES中有但数据库中没有) +//func findESOnlyISBNs(esISBNs []string, dbExistsMap map[string]bool) []string { +// var esOnlyISBNs []string +// +// for _, isbn := range esISBNs { +// if !dbExistsMap[isbn] { +// esOnlyISBNs = append(esOnlyISBNs, isbn) +// } +// } +// +// log.Printf("ES中有 %d 个ISBN在数据库中不存在", len(esOnlyISBNs)) +// return esOnlyISBNs +//} +// +//// 导出ES独有ISBN到txt文件 +//func exportESOnlyISBNs(esOnlyISBNs []string, outputFile string) error { +// if len(esOnlyISBNs) == 0 { +// log.Printf("没有ES独有的ISBN需要导出") +// return nil +// } +// +// // 确保输出目录存在 +// outputDir := filepath.Dir(outputFile) +// if err := os.MkdirAll(outputDir, 0755); err != nil { +// return fmt.Errorf("创建输出目录失败: %w", err) +// } +// +// // 写入文件 +// file, err := os.Create(outputFile) +// if err != nil { +// return fmt.Errorf("创建文件失败: %w", err) +// } +// defer file.Close() +// +// // 写入文件头信息 +// header := fmt.Sprintf(`# ES中有但数据库中没有的ISBN列表 +//# 数据库表: xgy_base_item +//# ES索引: %s +//# 导出时间: %s +//# 记录数: %d +//# 说明: 这些ISBN在ES索引中存在但在数据库表中不存在 +// +//`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(esOnlyISBNs)) +// +// if _, err := file.WriteString(header); err != nil { +// return fmt.Errorf("写入文件头失败: %w", err) +// } +// +// // 按字母顺序排序后写入 +// sort.Strings(esOnlyISBNs) +// successCount := 0 +// for _, isbn := range esOnlyISBNs { +// if _, err := file.WriteString(isbn + "\n"); err != nil { +// log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) +// continue +// } +// successCount++ +// } +// +// log.Printf("成功导出 %d/%d 个ES独有ISBN到文件: %s", successCount, len(esOnlyISBNs), outputFile) +// return nil +//} +// +//// 主函数:查询ES中有但数据库中没有的ISBN +//func mainFindESOnlyISBNs() { +// // 初始化数据库连接 +// dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", +// "root", +// "123456", +// "localhost", +// 3306, +// "book_image") // 请根据实际情况修改数据库名 +// +// db, err := sql.Open("mysql", dsn) +// if err != nil { +// log.Fatalf("打开数据库连接失败: %v", err) +// } +// defer db.Close() +// +// // 设置连接池参数 +// db.SetMaxOpenConns(20) +// db.SetMaxIdleConns(10) +// +// // 测试数据库连接 +// if err := db.Ping(); err != nil { +// log.Fatalf("数据库连接测试失败: %v", err) +// } +// +// // 初始化ES客户端 +// es, err := NewESClient([]string{esAddress}, esUsername, esPassword) +// if err != nil { +// log.Fatalf("ES连接失败: %v", err) +// } +// +// // 检查ES健康状态 +// if err := es.CheckHealth(); err != nil { +// log.Fatalf("ES健康检查失败: %v", err) +// } +// +// startTime := time.Now() +// log.Printf("开始处理ES与数据库的ISBN匹配...") +// +// // 步骤1: 从ES获取所有ISBN +// esISBNs, err := getAllISBNsFromES(es) +// if err != nil { +// log.Fatalf("获取ES ISBN失败: %v", err) +// } +// +// // 步骤2: 检查ISBN在数据库中的存在情况 +// dbExistsMap, err := checkISBNsInDB(db, esISBNs) +// if err != nil { +// log.Fatalf("检查数据库中ISBN存在情况失败: %v", err) +// } +// +// // 步骤3: 找出ES独有ISBN(ES中有但数据库中没有) +// esOnlyISBNs := findESOnlyISBNs(esISBNs, dbExistsMap) +// +// // 步骤4: 导出ES独有ISBN +// outputFile := "cmd/update_es_gt/missing_isbns.txt" +// if err := exportESOnlyISBNs(esOnlyISBNs, outputFile); err != nil { +// log.Fatalf("导出ES独有ISBN失败: %v", err) +// } +// +// elapsed := time.Since(startTime) +// +// // 输出统计信息 +// fmt.Printf("\n=== 处理完成 ===\n") +// fmt.Printf("ES中ISBN总数: %d\n", len(esISBNs)) +// fmt.Printf("数据库中匹配的ISBN数: %d\n", len(dbExistsMap)) +// fmt.Printf("ES独有ISBN数(数据库中没有的): %d\n", len(esOnlyISBNs)) +// fmt.Printf("独有比例: %.2f%%\n", float64(len(esOnlyISBNs))/float64(len(esISBNs))*100) +// fmt.Printf("耗时: %v\n", elapsed.Round(time.Millisecond)) +// fmt.Printf("输出文件: %s\n", outputFile) +// +// // 显示部分ES独有ISBN示例 +// if len(esOnlyISBNs) > 0 { +// fmt.Printf("\nES独有ISBN示例 (前10个):\n") +// for i := 0; i < 10 && i < len(esOnlyISBNs); i++ { +// fmt.Printf(" %s\n", esOnlyISBNs[i]) +// } +// if len(esOnlyISBNs) > 10 { +// fmt.Printf(" ... 还有 %d 个\n", len(esOnlyISBNs)-10) +// } +// } +//} +// +//// 查询并导出有销售记录且book_pic为空的ISBN +//func queryAndExportSaleISBNsWithEmptyPic(es *ESClient, outputFile string) error { +// log.Printf("开始查询有销售记录且book_pic为空的ISBN...") +// +// // 查询条件: (day_sale_7 > 0 OR day_sale_15 > 0 OR day_sale_30 > 0) AND book_pic为空 +// query := map[string]interface{}{ +// "query": map[string]interface{}{ +// "bool": map[string]interface{}{ +// "must": []map[string]interface{}{ +// { +// "bool": map[string]interface{}{ +// "should": []map[string]interface{}{ +// {"range": map[string]interface{}{"day_sale_7": map[string]interface{}{"gt": 0}}}, +// {"range": map[string]interface{}{"day_sale_15": map[string]interface{}{"gt": 0}}}, +// {"range": map[string]interface{}{"day_sale_30": map[string]interface{}{"gt": 0}}}, +// }, +// "minimum_should_match": 1, +// }, +// }, +// { +// "bool": map[string]interface{}{ +// "should": []map[string]interface{}{ +// // 匹配book_pic字段为空 +// {"term": map[string]interface{}{"book_pic": ""}}, +// // 匹配book_pic字段不存在 +// { +// "bool": map[string]interface{}{ +// "must_not": map[string]interface{}{ +// "exists": map[string]interface{}{"field": "book_pic"}, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// "_source": []string{"isbn"}, +// "sort": []map[string]interface{}{ +// {"isbn": "asc"}, // 按ISBN排序 +// }, +// "size": 10000, +// } +// +// // 打印查询条件用于验证 +// queryJSON, _ := json.MarshalIndent(query, "", " ") +// log.Printf("查询条件:\n%s", string(queryJSON)) +// +// var allISBNs []string +// var searchAfter interface{} +// totalCount := 0 +// page := 1 +// +// for { +// // 复制基础查询 +// currentQuery := make(map[string]interface{}) +// for k, v := range query { +// currentQuery[k] = v +// } +// +// // 添加游标 +// if searchAfter != nil { +// currentQuery["search_after"] = searchAfter +// } +// +// body, err := json.Marshal(currentQuery) +// if err != nil { +// return fmt.Errorf("序列化查询失败: %w", err) +// } +// +// log.Printf("执行第 %d 页查询...", page) +// +// // 执行搜索 +// res, err := es.client.Search( +// es.client.Search.WithIndex(esIndex), +// es.client.Search.WithBody(strings.NewReader(string(body))), +// es.client.Search.WithContext(context.Background()), +// ) +// if err != nil { +// return fmt.Errorf("ES搜索失败: %w", err) +// } +// defer res.Body.Close() +// +// if res.IsError() { +// bodyBytes, _ := io.ReadAll(res.Body) +// return fmt.Errorf("ES搜索返回错误: %s, 响应: %s", res.String(), string(bodyBytes)) +// } +// +// // 读取并解析响应体 +// bodyBytes, err := io.ReadAll(res.Body) +// if err != nil { +// return fmt.Errorf("读取响应体失败: %w", err) +// } +// +// var result map[string]interface{} +// if err := json.Unmarshal(bodyBytes, &result); err != nil { +// return fmt.Errorf("解析ES响应失败: %w", err) +// } +// +// // 检查是否有错误 +// if errMsg, exists := result["error"]; exists { +// return fmt.Errorf("ES返回错误: %v", errMsg) +// } +// +// hits, ok := result["hits"].(map[string]interface{}) +// if !ok { +// return fmt.Errorf("无法解析hits字段") +// } +// +// // 获取总命中数 +// if totalHits, exists := hits["total"].(map[string]interface{}); exists { +// if totalValue, exists := totalHits["value"]; exists { +// log.Printf("ES返回总命中数: %.0f", totalValue) +// } +// } +// +// hitList, ok := hits["hits"].([]interface{}) +// if !ok || len(hitList) == 0 { +// log.Printf("第 %d 页没有数据,查询完成", page) +// break // 没有更多数据 +// } +// +// // 处理当前批次的数据 +// batchCount := 0 +// for _, hit := range hitList { +// hitMap, ok := hit.(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析hit数据") +// continue +// } +// +// source, ok := hitMap["_source"].(map[string]interface{}) +// if !ok { +// log.Printf("警告: 无法解析_source字段") +// continue +// } +// +// isbn, ok := source["isbn"].(string) +// if ok && isbn != "" { +// allISBNs = append(allISBNs, isbn) +// batchCount++ +// } else { +// log.Printf("警告: 跳过空的ISBN字段") +// } +// +// // 更新游标(使用最后一个文档的排序值) +// sortValues, ok := hitMap["sort"].([]interface{}) +// if ok && len(sortValues) > 0 { +// searchAfter = sortValues +// } +// } +// +// totalCount += batchCount +// log.Printf("第 %d 页: 获取 %d 条ISBN记录,总计: %d", page, batchCount, totalCount) +// page++ +// +// // 如果返回的数量小于请求的数量,说明已经是最后一页 +// if len(hitList) < 10000 { +// log.Printf("最后一页数据量 %d < 10000,查询完成", len(hitList)) +// break +// } +// +// // 添加短暂延迟,避免对ES造成过大压力 +// time.Sleep(100 * time.Millisecond) +// } +// +// if len(allISBNs) == 0 { +// return fmt.Errorf("没有找到符合条件的ISBN记录") +// } +// +// // 去重 +// isbnSet := make(map[string]bool) +// uniqueISBNs := make([]string, 0) +// for _, isbn := range allISBNs { +// if !isbnSet[isbn] { +// isbnSet[isbn] = true +// uniqueISBNs = append(uniqueISBNs, isbn) +// } +// } +// +// log.Printf("去重前: %d 条, 去重后: %d 条", len(allISBNs), len(uniqueISBNs)) +// +// // 确保输出目录存在 +// outputDir := filepath.Dir(outputFile) +// if err := os.MkdirAll(outputDir, 0755); err != nil { +// return fmt.Errorf("创建输出目录失败: %w", err) +// } +// +// // 写入文件 +// file, err := os.Create(outputFile) +// if err != nil { +// return fmt.Errorf("创建文件失败: %w", err) +// } +// defer file.Close() +// +// // 写入文件头信息 +// header := fmt.Sprintf(`# 有销售记录且book_pic为空的ISBN列表 +//# 查询条件: (day_sale_7 > 0 OR day_sale_15 > 0 OR day_sale_30 > 0) AND (book_pic为空 或 book_pic字段不存在) +//# 索引: %s +//# 查询时间: %s +//# 总记录数: %d +// +//`, esIndex, time.Now().Format("2006-01-02 15:04:05"), len(uniqueISBNs)) +// +// if _, err := file.WriteString(header); err != nil { +// return fmt.Errorf("写入文件头失败: %w", err) +// } +// +// // 按字母顺序排序后写入 +// sort.Strings(uniqueISBNs) +// successCount := 0 +// for _, isbn := range uniqueISBNs { +// if _, err := file.WriteString(isbn + "\n"); err != nil { +// log.Printf("警告: 写入ISBN失败 %s: %v", isbn, err) +// continue +// } +// successCount++ +// } +// +// log.Printf("成功导出 %d/%d 个符合条件的ISBN到文件: %s", successCount, len(uniqueISBNs), outputFile) +// return nil +//} +// +//// 查询并导出有销售记录且book_pic为空的ISBN主函数 +//func mainQuerySaleISBNsWithEmptyPic() { +// // 初始化ES客户端 +// es, err := NewESClient([]string{esAddress}, esUsername, esPassword) +// if err != nil { +// log.Fatalf("ES连接失败: %v", err) +// } +// +// // 检查ES健康状态 +// if err := es.CheckHealth(); err != nil { +// log.Fatalf("ES健康检查失败: %v", err) +// } +// +// // 输出文件路径 +// outputFile := "es/sale_isbns_empty_pic.txt" +// +// // 查询并导出ISBN +// startTime := time.Now() +// if err := queryAndExportSaleISBNsWithEmptyPic(es, outputFile); err != nil { +// log.Fatalf("导出有销售记录且book_pic为空的ISBN失败: %v", err) +// } +// +// elapsed := time.Since(startTime) +// log.Printf("任务完成!耗时: %v,ISBN已导出到: %s", elapsed.Round(time.Millisecond), outputFile) +//} diff --git a/es/test.go b/es/test.go new file mode 100644 index 0000000..01d0e7e --- /dev/null +++ b/es/test.go @@ -0,0 +1,207 @@ +package main + +import ( + "time" +) + +//func main() { + +//client, err := newESClient([]string{esAddress}, esUsername, esPassword) +//if err != nil { +// fmt.Println(err.Error()) +//} +//fmt.Println(client) +// +//// 获取所有索引 +//indices, err := client.listAllIndices() +//if err != nil { +// fmt.Println(err.Error()) +//} +//for _, i := range indices { +// fmt.Println("索引:", i) +//} +//fmt.Println("所有索引:", indices) + +// // 获取所有索引的详细信息 +// //infos, err := client.GetIndicesInfo() +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// //infosJson, _ := json.Marshal(infos) +// //fmt.Println("所有索引的详细信息:", string(infosJson)) +// +// // 获取索引设置 +// //settings, err := client.GetIndexSettings("test-go-index") +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// //marshal, _ := json.Marshal(settings) +// //fmt.Println("marshal:", string(marshal)) +// +// // 创建索引 +// // 创建索引映射 +// //mapping := `{ +// // "settings": { +// // "number_of_shards": 1, +// // "number_of_replicas": 1, +// // "analysis": { +// // "analyzer": { +// // "default": { +// // "type": "standard" +// // } +// // } +// // } +// // }, +// // "mappings": { +// // "properties": { +// // "id":{ +// // "type":"integer" +// // }, +// // "title": { +// // "type": "text", +// // "analyzer": "standard", +// // "search_analyzer": "standard" +// // }, +// // "content": { +// // "type": "text", +// // "analyzer": "standard", +// // "search_analyzer": "standard" +// // }, +// // "author": { +// // "type": "keyword" +// // }, +// // "created_at": { +// // "type": "date" +// // }, +// // "tags": { +// // "type": "keyword" +// // } +// // } +// // } +// //}` +// //index, err := client.CreateIndex("test-cc", mapping) +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// //marshal, _ := json.Marshal(index) +// //fmt.Println("marshal:", string(marshal)) +// +// 删除索引 +//err = client.deleteIndex("test-go-index") +//if err != nil { +// fmt.Println(err.Error()) +//} +// +// // 修改索引设置 +// //newSettings := map[string]interface{}{ +// // "index": map[string]interface{}{ +// // "refresh_interval": "5s", +// // "max_result_window": 10000, +// // }, +// //} +// //err = client.UpdateIndexSettings("test-cc", newSettings) +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// // 更新映射 +// //newMappings := map[string]interface{}{ +// // "views": map[string]interface{}{ +// // "type": "integer", +// // }, +// //} +// //err = client.UpdateIndexMappings("test-cc", newMappings) +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// // 关闭索引 +// //err = client.CloseIndex("test-cc") +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// //// 打开索引 +// //err = client.OpenIndex("test-cc") +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// // 获取文档数量 +// //count, err := client.GetDocumentCount("test-cc") +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// //fmt.Println("文档数量: ", count) +// +// // 创建文档 +// //doc := Document{ +// // ID: "10002", +// // Title: "测试文档2", +// // Content: "这是一个测试文档2", +// // Author: "测试员2", +// // CreatedAt: time.Now(), +// // Tags: []string{"测试2", "文档2"}, +// //} +// //err = client.CreateDocument("test-cc", doc.ID, doc) +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// // 更新文档 +// //err = client.UpdateDocument("test-cc", "10002", map[string]interface{}{ +// // "title": "测试文档2", +// // "content": "这是一个测试文档2", +// //}) +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// // 删除文档 +// //err = client.DeleteDocument("test-cc", "10001") +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// +// // 搜索文档 +// //query := map[string]interface{}{ +// // "query": map[string]interface{}{ +// // "match": map[string]interface{}{ +// // "id": "10000", +// // }, +// // }, +// // "size": 100, +// //} +// //documents, err := client.SearchDocuments("test-cc", query) +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// //documentsJson, _ := json.Marshal(documents) +// //fmt.Println(string(documentsJson)) +// +// // 获取文档信息 +// //document, err := client.GetDocument("test-cc", "10000") +// //if err != nil { +// // fmt.Println(err.Error()) +// //} +// //bytes, _ := json.Marshal(document) +// //fmt.Println("获取文档信息", string(bytes)) +// +// // 获取单个索引 +// detail, err := client.getIndexDetail("test-cc") +// if err != nil { +// fmt.Println(err.Error()) +// } +// detailJson, _ := json.Marshal(detail) +// fmt.Println("单个索引的所有信息:", string(detailJson)) +//} + +// Document 示例文档结构 +type Document struct { + ID string `json:"id,omitempty"` + Title string `json:"title"` + Content string `json:"content"` + Author string `json:"author"` + CreatedAt time.Time `json:"created_at"` + Tags []string `json:"tags"` +} diff --git a/gin/gin.go b/gin/gin.go new file mode 100644 index 0000000..ae757be --- /dev/null +++ b/gin/gin.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/gin-gonic/gin" + //"net/http" +) + +func main() { + // 创建 Gin 实例 + r := gin.Default() + + // 定义路由 + r.GET("/ping", func(c *gin.Context) { + c.JSON(200, gin.H{ + "message": "pong", + }) + }) + + // XML 响应 + r.GET("/xml", func(c *gin.Context) { + c.XML(200, gin.H{ + "message": "hello", + "status": "success", + }) + }) + + // 启动服务 + r.Run(":8080") // 默认监听 8080 端口 +} diff --git a/go.mod b/go.mod index 476e6cf..cf5ffc4 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,12 @@ require ( github.com/chromedp/chromedp v0.14.2 github.com/disintegration/imaging v1.6.2 github.com/elastic/go-elasticsearch/v8 v8.19.0 + github.com/gin-gonic/gin v1.11.0 github.com/go-sql-driver/mysql v1.9.3 + github.com/makiuchi-d/gozxing v0.1.1 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/parnurzeal/gorequest v0.3.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/xuri/excelize/v2 v2.10.0 golang.org/x/image v0.25.0 golang.org/x/sys v0.37.0 @@ -18,28 +21,56 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect github.com/chromedp/sysutil v1.1.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/elastic/elastic-transport-go/v8 v8.7.0 // indirect github.com/elazarl/goproxy v1.7.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // 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.27.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.4.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.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-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/moul/http2curl v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect github.com/smartystreets/goconvey v1.8.1 // indirect github.com/tiendc/go-deepcopy v1.7.1 // 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.2-0.20250530014748-2ddeb826f9a9 // 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 + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/go.sum b/go.sum index 6ca92c1..843f618 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,19 @@ github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiU github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E= github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= @@ -20,6 +27,12 @@ github.com/elastic/go-elasticsearch/v8 v8.19.0 h1:VmfBLNRORY7RZL+9hTxBD97ehl9H8N github.com/elastic/go-elasticsearch/v8 v8.19.0/go.mod h1:F3j9e+BubmKvzvLjNui/1++nJuJxbkhHefbaT0kFKGY= github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +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.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -27,6 +40,14 @@ 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.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -35,14 +56,34 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +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/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +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/makiuchi-d/gozxing v0.1.1 h1:xxqijhoedi+/lZlhINteGbywIrewVdVv2wl9r5O9S1I= +github.com/makiuchi-d/gozxing v0.1.1/go.mod h1:eRIHbOjX7QWxLIDJoQuMLhuXg9LAuw6znsUtRkNw9DU= +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 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/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/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -51,23 +92,42 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/parnurzeal/gorequest v0.3.0 h1:SoFyqCDC9COr1xuS6VA8fC8RU7XyrJZN2ona1kEX7FI= github.com/parnurzeal/gorequest v0.3.0/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= +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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= 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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +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.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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4= github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= +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.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4= @@ -83,6 +143,10 @@ go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZ 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= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= @@ -99,6 +163,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -117,6 +183,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -157,6 +225,14 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= diff --git a/image/image.go b/image/image.go index 348d712..4b93ed3 100644 --- a/image/image.go +++ b/image/image.go @@ -6,6 +6,8 @@ import ( "encoding/json" "fmt" "github.com/disintegration/imaging" + "github.com/makiuchi-d/gozxing" + "github.com/makiuchi-d/gozxing/qrcode" "github.com/nfnt/resize" "golang.org/x/image/draw" "image" @@ -14,6 +16,7 @@ import ( _ "image/jpeg" "image/png" _ "image/png" + "math" "os" "path/filepath" "strings" @@ -22,16 +25,18 @@ import ( // Config 配置结构体 type Config struct { - OutputDir string // 输出目录路径 - FileName string // 文件名 - MatchDir string // 满足条件的图片目录名 - UnmatchDir string // 不满足条件的图片目录名 - EqualHeightDir string // 等高的图片目录名 - WhiteDir string // 白色底图的图片目录名 - WhiteBorderPngDir string // 去白边转PNG的图片目录名 - MinWhitePct float64 // 纯白占比下限(0-1) - MaxWhitePct float64 // 纯白占比上限(0-1) - Extensions []string // 支持的图片扩展名 + OutputDir string // 输出目录路径 + FileName string // 文件名 + MatchDir string // 满足条件的图片目录名 + UnmatchDir string // 不满足条件的图片目录名 + EqualHeightDir string // 等高的图片目录名 + WhiteDir string // 白色底图的图片目录名 + WhiteBorderPngDir string // 去白边转PNG的图片目录名 + WhiteHeightZoomDir string // 缩放的图片目录 + CropDir string // 裁切的图片目录 + MinWhitePct float64 // 纯白占比下限(0-1) + MaxWhitePct float64 // 纯白占比上限(0-1) + Extensions []string // 支持的图片扩展名 } // 检查图片 @@ -52,6 +57,7 @@ func validateConfig(config *Config) error { return nil } +// 创建目录功能 func createDirs(config *Config) error { // 创建输出根目录 if err := os.MkdirAll(config.OutputDir, 0755); err != nil { @@ -84,6 +90,16 @@ func createDirs(config *Config) error { if err := os.MkdirAll(whiteBorderPngPath, 0755); err != nil { return err } + + whiteHeightZoomPath := filepath.Join(config.OutputDir, config.WhiteHeightZoomDir) + if err := os.MkdirAll(whiteHeightZoomPath, 0755); err != nil { + return err + } + + cropPath := filepath.Join(config.OutputDir, config.CropDir) + if err := os.MkdirAll(cropPath, 0755); err != nil { + return err + } return nil } @@ -503,6 +519,596 @@ func (c *ImageToPNGConverter) isBackgroundColor(pixel color.Color, hasAlpha bool b8 >= threshold8 } +// 图片缩放 +func resizeWTToHeightQuality(config *Config, dsWidth, dsHeight int) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + fmt.Printf("创建目录失败: %v\n", err) + os.Exit(1) + } + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, format, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + // 使用Lanczos3算法缩放图片 + optimized := resizeImageOptimized(img, dsWidth, dsHeight) + + // 保存图片到指定目录下 + filename := filepath.Base(config.FileName) + destPath := filepath.Join(config.OutputDir, config.WhiteHeightZoomDir, filename) + err = saveImage(destPath, optimized, format) + if err != nil { + return "", fmt.Errorf("保存图片失败: %v", err) + } + + return destPath, nil +} + +// 使用Lanczos3算法缩放图片 +func resizeImageOptimized(src image.Image, dstWidth, dstHeight int) image.Image { + srcBounds := src.Bounds() + srcWidth := srcBounds.Dx() + srcHeight := srcBounds.Dy() + + // 创建目标图片 + dst := image.NewRGBA(image.Rect(0, 0, dstWidth, dstHeight)) + + // 计算缩放比例 + xScale := float64(srcWidth) / float64(dstWidth) + yScale := float64(srcHeight) / float64(dstHeight) + + // Lanczos3算法半径 + radius := 3.0 + + // 预计算x方向的权重 + xWeights := make([][]float64, dstWidth) + xIndices := make([][]int, dstWidth) + + for x := 0; x < dstWidth; x++ { + srcX := float64(x) * xScale + startX := int(math.Max(0, math.Floor(srcX-radius+0.5))) + endX := int(math.Min(float64(srcWidth-1), math.Floor(srcX+radius))) + + weights := make([]float64, 0, endX-startX+1) + indices := make([]int, 0, endX-startX+1) + + for sx := startX; sx <= endX; sx++ { + xDist := float64(sx) + 0.5 - srcX + weight := lanczos3(xDist) + if weight != 0 { + weights = append(weights, weight) + indices = append(indices, sx) + } + } + + xWeights[x] = weights + xIndices[x] = indices + } + + // 处理每一行 + for y := 0; y < dstHeight; y++ { + srcY := float64(y) * yScale + startY := int(math.Max(0, math.Floor(srcY-radius+0.5))) + endY := int(math.Min(float64(srcHeight-1), math.Floor(srcY+radius))) + + // 预计算y方向的权重 + yWeights := make([]float64, 0, endY-startY+1) + yIndices := make([]int, 0, endY-startY+1) + + for sy := startY; sy <= endY; sy++ { + yDist := float64(sy) + 0.5 - srcY + weight := lanczos3(yDist) + if weight != 0 { + yWeights = append(yWeights, weight) + yIndices = append(yIndices, sy) + } + } + + // 处理每一列 + for x := 0; x < dstWidth; x++ { + var rSum, gSum, bSum, aSum, weightSum float64 + + // 应用预计算的权重 + for i, sy := range yIndices { + yWeight := yWeights[i] + + for j, sx := range xIndices[x] { + xWeight := xWeights[x][j] + weight := xWeight * yWeight + + // 获取源像素颜色 + srcColor := src.At(sx+srcBounds.Min.X, sy+srcBounds.Min.Y) + r, g, b, a := srcColor.RGBA() + + // 累加加权颜色值 + rSum += float64(r>>8) * weight + gSum += float64(g>>8) * weight + bSum += float64(b>>8) * weight + aSum += float64(a>>8) * weight + weightSum += weight + } + } + + // 防止除以零 + if weightSum == 0 { + weightSum = 1 + } + + // 计算最终颜色值 + r := clamp(rSum / weightSum) + g := clamp(gSum / weightSum) + b := clamp(bSum / weightSum) + a := clamp(aSum / weightSum) + + // 设置目标像素 + dst.SetRGBA(x, y, color.RGBA{r, g, b, a}) + } + } + + return dst +} + +// 函数计算Lanczos3核函数值 +func lanczos3(x float64) float64 { + if x == 0 { + return 1.0 + } + if x < -3 || x > 3 { + return 0.0 + } + return (3 * math.Sin(math.Pi*x) * math.Sin(math.Pi*x/3)) / (math.Pi * math.Pi * x * x) +} + +// 将值限制在0-255范围内 +func clamp(v float64) uint8 { + if v < 0 { + return 0 + } + if v > 255 { + return 255 + } + return uint8(v + 0.5) +} + +// 图片裁切 +func cropImage(config *Config, x, y, width, height int) (string, error) { + // 创建输出目录 + if err := createDirs(config); err != nil { + fmt.Printf("创建目录失败: %v\n", err) + os.Exit(1) + } + + // 打开图片 + file, err := os.Open(config.FileName) + if err != nil { + return "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + // 解码原始图片 + img, format, err := image.Decode(file) + if err != nil { + return "", fmt.Errorf("图片解码失败: %v", err) + } + + fmt.Printf("输入图片: %s (%dx%d, 格式: %s)\n", + config.FileName, img.Bounds().Dx(), img.Bounds().Dy(), format) + + imgFile, err := basicCrop(img, x, y, width, height) + if err != nil { + return "", err + } + + //// 执行裁切 + //var cropped image.Image + //if width > 0 && height > 0 { + // cropped, err = SmartCrop(img, x, y, width, height int) + //} else { + // // 如果未指定尺寸,使用中心裁切 + // cropped, err = CropCenter(img, config.Width, config.Height) + //} + // + //if err != nil { + // return "", err + //} + // + + // 保存图片到指定目录下 + filename := filepath.Base(config.FileName) + destPath := filepath.Join(config.OutputDir, config.CropDir, filename) + err = saveImage(destPath, imgFile, format) + if err != nil { + return "", fmt.Errorf("保存图片失败: %v", err) + } + return destPath, nil +} + +// basicCrop 基础裁切功能 +func basicCrop(src image.Image, x, y, width, height int) (image.Image, error) { + srcBounds := src.Bounds() + srcWidth := srcBounds.Dx() + srcHeight := srcBounds.Dy() + + // 验证裁切参数 + if x < 0 || y < 0 || width <= 0 || height <= 0 { + return nil, fmt.Errorf("invalid crop parameters: x=%d, y=%d, width=%d, height=%d", + x, y, width, height) + } + + if x >= srcWidth || y >= srcHeight { + return nil, fmt.Errorf("crop start point (%d, %d) is outside image bounds (%dx%d)", + x, y, srcWidth, srcHeight) + } + + // 调整裁切尺寸以避免超出边界 + if x+width > srcWidth { + width = srcWidth - x + } + if y+height > srcHeight { + height = srcHeight - y + } + + // 从源图像中裁切 + cropped := image.NewRGBA(image.Rect(0, 0, width, height)) + for cy := 0; cy < height; cy++ { + for cx := 0; cx < width; cx++ { + cropped.Set(cx, cy, src.At(x+cx, y+cy)) + } + } + + return cropped, nil +} + +//// smartCrop 智能裁切,处理边界和自动调整 -- 暂时没用 +//func smartCrop(src image.Image, x, y, width, height int) (image.Image, error) { +// srcBounds := src.Bounds() +// srcWidth := srcBounds.Dx() +// srcHeight := srcBounds.Dy() +// +// // 如果宽度或高度为0,使用图像的最大可能尺寸 +// if width == 0 { +// width = srcWidth - x +// } +// if height == 0 { +// height = srcHeight - y +// } +// +// // 调整裁切区域以确保在边界内 +// newX := max(0, min(x, srcWidth-1)) +// newY := max(0, min(x, srcHeight-1)) +// width = max(1, min(width, srcWidth-x)) +// height = max(1, min(height, srcHeight-y)) +// +// // 如果需要保持宽高比,调整裁切区域 +// if width > 0 && height > 0 { +// currentRatio := float64(width) / float64(height) +// originalRatio := float64(srcWidth) / float64(srcHeight) +// +// if math.Abs(currentRatio-originalRatio) > 0.01 { +// // 调整宽度以匹配原始宽高比 +// newWidth := int(float64(height) * originalRatio) +// if newWidth <= (srcWidth - newX) { +// width = newWidth +// } else { +// // 调整高度以匹配原始宽高比 +// newHeight := int(float64(width) / originalRatio) +// if newHeight <= (srcHeight - newY) { +// height = newHeight +// } +// } +// } +// } +// +// // 执行裁切 +// cropped, err := basicCrop(src, newX, newY, width, height) +// if err != nil { +// return nil, err +// } +// +// return cropped, nil +//} + +//// CropCenter 中心裁切 -- 暂时没用 +//func CropCenter(src image.Image, width, height int) (image.Image, error) { +// srcBounds := src.Bounds() +// srcWidth := srcBounds.Dx() +// srcHeight := srcBounds.Dy() +// +// // 如果裁切尺寸大于原图,返回原图或调整裁切尺寸 +// if width >= srcWidth && height >= srcHeight { +// return src, nil +// } +// +// // 计算中心裁切的起始点 +// x := (srcWidth - width) / 2 +// y := (srcHeight - height) / 2 +// +// // 确保不超出边界 +// x = max(0, min(x, srcWidth-width)) +// y = max(0, min(y, srcHeight-height)) +// +// return BasicCrop(src, x, y, width, height) +//} + +// 识别二维码 +func scanQRCode(fileName string) (bool, string, error) { + file, err := os.Open(fileName) + if err != nil { + return false, "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + + // 解码图像 + img, _, err := image.Decode(file) + if err != nil { + return false, "", fmt.Errorf("解码图片失败: %v", err) + } + + // 创建二维码读取器 + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return false, "", fmt.Errorf("创建位图失败: %v", err) + } + + // 解码二维码 + reader := qrcode.NewQRCodeReader() + + result, err := reader.Decode(bmp, nil) + if err != nil { + return false, "", analyzeQRCodeError(err) + } + + // 6. 打印二维码内容 + fmt.Printf("二维码内容: %s\n", result.GetText()) + + // 7. 获取二维码位置点 + points := result.GetResultPoints() + if len(points) < 3 { + fmt.Println("未找到足够的定位点") + } + + return true, result.GetText(), nil +} + +// 识别二维码 +func scanQRCodeNew(fileName string) (bool, string, error) { + file, err := os.Open(fileName) + if err != nil { + return false, "", fmt.Errorf("打开图片失败: %v", err) + } + defer file.Close() + + // 解码图像 + img, _, err := image.Decode(file) + if err != nil { + return false, "", fmt.Errorf("解码图片失败: %v", err) + } + + // 创建二维码读取器 + bmp, err := gozxing.NewBinaryBitmapFromImage(img) + if err != nil { + return false, "", fmt.Errorf("创建位图失败: %v", err) + } + + // 解码二维码 + reader := qrcode.NewQRCodeReader() + + result, err := reader.Decode(bmp, nil) + if err != nil { + return false, "", analyzeQRCodeError(err) + } + + // 6. 打印二维码内容 + fmt.Printf("二维码内容: %s\n", result.GetText()) + + // 7. 获取二维码位置点 + points := result.GetResultPoints() + if len(points) < 3 { + fmt.Println("未找到足够的定位点") + } + + // 8. 计算二维码边界框 + minX, minY := int(points[0].GetX()), int(points[0].GetY()) + maxX, maxY := minX, minY + + for _, point := range points { + x, y := int(point.GetX()), int(point.GetY()) + if x < minX { + minX = x + } + if x > maxX { + maxX = x + } + if y < minY { + minY = y + } + if y > maxY { + maxY = y + } + } + + // 9. 添加一些边距(可选) + margin := 10 + minX -= margin + minY -= margin + maxX += margin + maxY += margin + + // 确保坐标不超出图片范围 + bounds := img.Bounds() + if minX < bounds.Min.X { + minX = bounds.Min.X + } + if minY < bounds.Min.Y { + minY = bounds.Min.Y + } + if maxX > bounds.Max.X { + maxX = bounds.Max.X + } + if maxY > bounds.Max.Y { + maxY = bounds.Max.Y + } + + // 10. 创建裁剪区域 + qrRect := image.Rect(minX, minY, maxX, maxY) + fmt.Printf("二维码位置: %v\n", qrRect) + + // 11. 创建新图片并复制二维码区域 + qrImg := image.NewRGBA(qrRect) + draw.Draw(qrImg, qrImg.Bounds(), img, qrRect.Min, draw.Src) + + // 12. 保存二维码图片 + outputFile, err := os.Create("image/qrcode.png") + if err != nil { + panic(err) + } + defer outputFile.Close() + + err = png.Encode(outputFile, qrImg) + if err != nil { + panic(err) + } + return true, result.GetText(), nil +} + +// 图像预处理 +func preprocessImage(img image.Image) image.Image { + // 转为灰度图 + gray := imaging.Grayscale(img) + + // 增强对比度 + enhanced := imaging.AdjustContrast(gray, 20) + + // 高斯模糊去噪 + blurred := imaging.Blur(enhanced, 1.0) + + // 自适应二值化 + binary := adaptiveThreshold(blurred) + + return binary +} + +// 二值化处理 +func adaptiveThreshold(img image.Image) image.Image { + bounds := img.Bounds() + dst := image.NewGray(bounds) + + // 局部自适应阈值 + blockSize := 15 + c := 2.0 + + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + // 计算局部均值 + sum := 0.0 + count := 0 + + for dy := -blockSize / 2; dy <= blockSize/2; dy++ { + for dx := -blockSize / 2; dx <= blockSize/2; dx++ { + nx, ny := x+dx, y+dy + if nx >= bounds.Min.X && nx < bounds.Max.X && + ny >= bounds.Min.Y && ny < bounds.Max.Y { + r, _, _, _ := img.At(nx, ny).RGBA() + sum += float64(r >> 8) + count++ + } + } + } + + localMean := sum / float64(count) + r, _, _, _ := img.At(x, y).RGBA() + gray := float64(r >> 8) + + if gray > localMean-c { + dst.SetGray(x, y, color.Gray{Y: 255}) + } else { + dst.SetGray(x, y, color.Gray{Y: 0}) + } + } + } + return dst +} + +// 分析二维码错误类型 +func analyzeQRCodeError(err error) error { + if err == nil { + return nil + } + // 检查不同类型的错误 + switch e := err.(type) { + case gozxing.ChecksumException: + return fmt.Errorf("二维码校验失败(可能部分损坏或被遮挡): %v", e) + case gozxing.FormatException: + return fmt.Errorf("二维码格式错误(可能不是有效的二维码): %v", e) + case gozxing.NotFoundException: + return fmt.Errorf("二维码格式错误: %v", e) + default: + return fmt.Errorf("解码失败: %v", err) + } +} + +// 生成二维码 +func generateQRCode(content string, width int, height int, fileName string) (string, error) { + // 创建 QR 码写入器 + writer := qrcode.NewQRCodeWriter() + + // 设置编码参数 + hints := make(map[gozxing.EncodeHintType]interface{}) + + // 纠错级别设置 + hints[gozxing.EncodeHintType_ERROR_CORRECTION] = "H" + + // 字符集 + hints[gozxing.EncodeHintType_CHARACTER_SET] = "UTF-8" + + // 边距 + hints[gozxing.EncodeHintType_MARGIN] = 4 + + // 生成二维码 + bitMatrix, err := writer.Encode(content, gozxing.BarcodeFormat_QR_CODE, width, height, hints) + if err != nil { + return "", fmt.Errorf("生成二维码失败: %v", err) + } + + // 创建图像 + img := image.NewRGBA(image.Rect(0, 0, width, height)) + + // 白色背景 + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + img.Set(x, y, color.White) + } + } + + // 黑色二维码点 + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + if bitMatrix.Get(x, y) { + img.Set(x, y, color.Black) + } + } + } + + // 保存文件 + file, err := os.Create(fileName) + if err != nil { + return "", fmt.Errorf("保存二维码失败: %v", err) + } + defer file.Close() + + png.Encode(file, img) + return fmt.Sprintf("生成二维码成功: %v", fileName), nil +} + +// =================== 辅助函数 ======================= + // 辅助函数 func absDiff(a, b uint8) uint8 { if a > b { @@ -578,6 +1184,44 @@ func RemoveWhiteBorderAndPNG(jsonConfig *C.char) *C.char { return C.CString(fileName) } +// ResizeWTToHeightQuality 图片缩放 +// +//export ResizeWTToHeightQuality +func ResizeWTToHeightQuality(jsonConfig *C.char, dsWidth, dsHeight C.int) *C.char { + configStr := C.GoString(jsonConfig) + dsWidthStr := int(dsWidth) + dsHeightStr := int(dsHeight) + var config *Config + if err := json.Unmarshal([]byte(configStr), &config); err != nil { + return C.CString(fmt.Sprintf("解析 config 失败: %v", err)) + } + fileName, err := resizeWTToHeightQuality(config, dsWidthStr, dsHeightStr) + if err != nil { + return C.CString(fmt.Sprintf("%v", err)) + } + return C.CString(fileName) +} + +// CropImage 图片裁切 +// +//export CropImage +func CropImage(jsonConfig *C.char, x, y, width, height C.int) *C.char { + configStr := C.GoString(jsonConfig) + xStr := int(x) + yStr := int(y) + widthStr := int(width) + heightStr := int(height) + var config *Config + if err := json.Unmarshal([]byte(configStr), &config); err != nil { + return C.CString(fmt.Sprintf("解析 config 失败: %v", err)) + } + fileName, err := cropImage(config, xStr, yStr, widthStr, heightStr) + if err != nil { + return C.CString(fmt.Sprintf("%v", err)) + } + return C.CString(fileName) +} + // 导出函数:释放C字符串内存 // //export FreeCString @@ -585,6 +1229,6 @@ func FreeCString(str *C.char) { C.free(unsafe.Pointer(str)) } +// main 函数是必需的,即使为空 //func main() { -// //} diff --git a/image/imageDllTest.go b/image/imageDllTest.go index 6487b00..e6384de 100644 --- a/image/imageDllTest.go +++ b/image/imageDllTest.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "fmt" + "github.com/skip2/go-qrcode" "os" "path/filepath" "syscall" @@ -80,30 +81,75 @@ func cStr(ptr uintptr) string { func main() { - config := &Config{ - OutputDir: "D:\\isbn_images\\result", // 输出根目录 - FileName: "D:\\isbn_images\\result\\9771671688095.jpg", - MatchDir: "matched", // 满足条件的图片目录 - UnmatchDir: "unmatched", // 不满足条件的图片目录 - WhiteDir: "white", - EqualHeightDir: "equalHeight", - MinWhitePct: 0.1, // 纯白占比下限 10% - MaxWhitePct: 0.65, // 纯白占比上限 90% - Extensions: []string{"jpg", "jpeg", "png", "gif", "bmp", "webp"}, - } + //config := &Config{ + // OutputDir: "D:\\isbn_images\\result", // 输出根目录 + // FileName: "D:\\isbn_images\\result\\97800079351851.jpg", + // //MatchDir: "matched", // 满足条件的图片目录 + // //UnmatchDir: "unmatched", // 不满足条件的图片目录 + // //WhiteDir: "white", + // //EqualHeightDir: "equalHeight", + // //WhiteHeightZoomDir: "whiteHeightZoom", + // CropDir: "crop", + // MinWhitePct: 0.1, // 纯白占比下限 10% + // MaxWhitePct: 0.65, // 纯白占比上限 90% + // Extensions: []string{"jpg", "jpeg", "png", "gif", "bmp", "webp"}, + //} - dll, err := InitImageDll() - if err != nil { - fmt.Println(err) - } - //err = dll.ProcessImage(config) + //dll, err := InitImageDll() //if err != nil { // fmt.Println(err) //} + ////err = dll.ProcessImage(config) + ////if err != nil { + //// fmt.Println(err) + ////} + // + //image, err := dll.CreateWhiteBottomCenteredImage(config, 800, 800) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(image) - image, err := dll.CreateWhiteBottomCenteredImage(config, 800, 800) + // 图片缩放 + //quality, err := resizeWTToHeightQuality(config, 800, 500) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(quality) + + //// 图片裁切 + //image, err := cropImage(config, 100, 0, 300, 300) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(image) + + //file := "D:\\isbn_images\\result\\123\\qrcode.jpg" + // + //code, s, err := scanQRCodeNew(file) + //if err != nil { + // fmt.Println(err) + //} + //if code { + // fmt.Println(s) + //} + + code, err := generateQRCode("你好", 500, 500, "D:\\isbn_images\\result\\123\\qrcode.jpg") if err != nil { fmt.Println(err) } - fmt.Println(image) + fmt.Println(code) +} + +// 生成二维码 +func generateQRCode1() { + // 一行代码生成二维码 + err := qrcode.WriteFile("https://example.com", qrcode.Medium, 256, "qrcode.png") + if err != nil { + panic(err) + } + + //// 更多控制 + //q, _ := qrcode.New("https://github.com", qrcode.High) + //q.WriteFile(256, "custom_qr.png") } diff --git a/image/imageTool.go b/image/imageTool.go deleted file mode 100644 index 7d7e3a4..0000000 --- a/image/imageTool.go +++ /dev/null @@ -1,644 +0,0 @@ -package main - -//import ( -// "fmt" -// "image" -// "image/color" -// "image/draw" -// "image/png" -// "io/ioutil" -// "os" -// "path/filepath" -// "strings" -// "sync" -// "time" -// -// "github.com/disintegration/imaging" -//) -// -////// ImageToPNGConverter 图片去白边并转为PNG -////type ImageToPNGConverter struct { -//// Threshold int -//// Margin int -//// BgColor color.RGBA -//// DetectColor *color.RGBA -//// KeepTransparent bool -//// PNGCompressLevel png.CompressionLevel -//// Quality int -////} -// -//// NewImageToPNGConverter 创建新的转换器 -//func NewImageToPNGConverter(threshold, margin int, bgColor, detectColor *color.RGBA, -// keepTransparent bool, compressLevel png.CompressionLevel, quality int) *ImageToPNGConverter { -// -// // 默认背景色为白色 -// bg := color.RGBA{R: 255, G: 255, B: 255, A: 255} -// if bgColor != nil { -// bg = *bgColor -// } -// -// return &ImageToPNGConverter{ -// Threshold: threshold, -// Margin: margin, -// BgColor: bg, -// DetectColor: detectColor, -// KeepTransparent: keepTransparent, -// PNGCompressLevel: compressLevel, -// Quality: quality, -// } -//} -// -//// IsBackgroundColor 判断像素是否为背景色 -//func (c *ImageToPNGConverter) IsBackgroundColor(pixel color.Color, hasAlpha bool) bool { -// r, g, b, a := pixel.RGBA() -// -// // 转换为8位值 -// r8 := uint8(r >> 8) -// g8 := uint8(g >> 8) -// b8 := uint8(b >> 8) -// a8 := uint8(a >> 8) -// -// // 检查透明度 -// if hasAlpha && a8 < 25 { // 透明度 > 90% -// return true -// } -// -// // 如果指定了检测颜色 -// if c.DetectColor != nil { -// dr, dg, db, _ := c.DetectColor.RGBA() -// dr8 := uint8(dr >> 8) -// dg8 := uint8(dg >> 8) -// db8 := uint8(db >> 8) -// -// // threshold 是 int 类型,需要转换为 uint8 比较 -// threshold8 := uint8(255 - c.Threshold) -// return absDiff(r8, dr8) <= threshold8 && -// absDiff(g8, dg8) <= threshold8 && -// absDiff(b8, db8) <= threshold8 -// } -// -// // 自动检测白色/浅色背景 -// // 注意:这里的 c.Threshold 是 int,需要转换为 uint8 -// threshold8 := uint8(c.Threshold) -// return r8 >= threshold8 && -// g8 >= threshold8 && -// b8 >= threshold8 -//} -// -//// FindBorders 查找图片的有效边界 -//func (c *ImageToPNGConverter) FindBorders(img image.Image) image.Rectangle { -// bounds := img.Bounds() -// width := bounds.Dx() -// height := bounds.Dy() -// -// // 检查图像是否有alpha通道 -// _, hasAlpha := img.(*image.NRGBA) -// if !hasAlpha { -// _, hasAlpha = img.(*image.RGBA) -// } -// -// // 初始化边界 -// left := width -// top := height -// right := 0 -// bottom := 0 -// -// // 查找非背景区域 -// for y := bounds.Min.Y; y < bounds.Max.Y; y++ { -// for x := bounds.Min.X; x < bounds.Max.X; x++ { -// pixel := img.At(x, y) -// if !c.IsBackgroundColor(pixel, hasAlpha) { -// if x < left { -// left = x -// } -// if x > right { -// right = x -// } -// if y < top { -// top = y -// } -// if y > bottom { -// bottom = y -// } -// } -// } -// } -// -// // 如果没有找到非背景区域,返回整个图像 -// if left > right || top > bottom { -// return bounds -// } -// -// // 添加边距 -// left = max(bounds.Min.X, left-c.Margin) -// top = max(bounds.Min.Y, top-c.Margin) -// right = min(bounds.Max.X, right+c.Margin+1) -// bottom = min(bounds.Max.Y, bottom+c.Margin+1) -// -// return image.Rect(left, top, right, bottom) -//} -// -//// TrimImage 裁剪图片白边 -//func (c *ImageToPNGConverter) TrimImage(img image.Image) image.Image { -// borders := c.FindBorders(img) -// -// // 创建一个新的图像并裁剪 -// trimmed := imaging.Crop(img, borders) -// return trimmed -//} -// -//// ConvertToPNG 转换图片为PNG格式 -//func (c *ImageToPNGConverter) ConvertToPNG(img image.Image, addBackground bool) image.Image { -// // 先裁剪白边 -// trimmed := c.TrimImage(img) -// -// // 检查是否有alpha通道 -// _, hasAlpha := trimmed.(*image.NRGBA) -// if !hasAlpha { -// _, hasAlpha = trimmed.(*image.RGBA) -// } -// -// if hasAlpha { -// if c.KeepTransparent { -// // 保持透明 -// return trimmed -// } else if addBackground { -// // 添加背景色 -// bg := image.NewRGBA(trimmed.Bounds()) -// draw.Draw(bg, bg.Bounds(), &image.Uniform{C: c.BgColor}, image.Point{}, draw.Src) -// draw.Draw(bg, bg.Bounds(), trimmed, trimmed.Bounds().Min, draw.Over) -// return bg -// } -// } else { -// // 非透明图像 -// if c.KeepTransparent { -// // 转换为RGBA -// rgba := image.NewRGBA(trimmed.Bounds()) -// draw.Draw(rgba, rgba.Bounds(), trimmed, trimmed.Bounds().Min, draw.Src) -// return rgba -// } -// return trimmed -// } -// -// return trimmed -//} -// -//// ProcessImageFile 处理单个图片文件 -//func (c *ImageToPNGConverter) ProcessImageFile(inputPath, outputPath string) map[string]interface{} { -// result := map[string]interface{}{ -// "success": false, -// "input_path": inputPath, -// "output_path": outputPath, -// } -// -// // 打开图片文件 -// file, err := os.Open(inputPath) -// if err != nil { -// result["error"] = err.Error() -// result["message"] = fmt.Sprintf("失败: %s - %s", filepath.Base(inputPath), err) -// return result -// } -// defer file.Close() -// -// // 解码图像 -// img, format, err := image.Decode(file) -// if err != nil { -// result["error"] = err.Error() -// result["message"] = fmt.Sprintf("失败: %s - %s", filepath.Base(inputPath), err) -// return result -// } -// -// // 获取原始信息 -// origBounds := img.Bounds() -// origSize := origBounds.Size() -// origArea := origSize.X * origSize.Y -// -// // 转换为PNG -// resultImg := c.ConvertToPNG(img, true) -// -// // 获取处理后的信息 -// newBounds := resultImg.Bounds() -// newSize := newBounds.Size() -// newArea := newSize.X * newSize.Y -// -// // 计算尺寸减少比例 -// sizeReduction := 0.0 -// if origArea > 0 { -// sizeReduction = 1 - float64(newArea)/float64(origArea) -// } -// -// // 获取原始文件大小 -// fileInfo, _ := os.Stat(inputPath) -// origFileSize := fileInfo.Size() -// -// // 保存为PNG -// outputFile, err := os.Create(outputPath) -// if err != nil { -// result["error"] = err.Error() -// result["message"] = fmt.Sprintf("失败: %s - %s", filepath.Base(inputPath), err) -// return result -// } -// defer outputFile.Close() -// -// encoder := png.Encoder{CompressionLevel: c.PNGCompressLevel} -// err = encoder.Encode(outputFile, resultImg) -// if err != nil { -// result["error"] = err.Error() -// result["message"] = fmt.Sprintf("失败: %s - %s", filepath.Base(inputPath), err) -// return result -// } -// -// // 获取新文件大小 -// newFileInfo, _ := os.Stat(outputPath) -// newFileSize := newFileInfo.Size() -// -// // 计算文件大小变化 -// fileSizeChange := 0.0 -// if origFileSize > 0 { -// fileSizeChange = float64(newFileSize) / float64(origFileSize) -// } -// -// result["success"] = true -// result["orig_format"] = format -// result["orig_size"] = origSize -// result["new_size"] = newSize -// result["size_reduction"] = sizeReduction -// result["orig_file_size"] = origFileSize -// result["new_file_size"] = newFileSize -// result["file_size_change"] = fileSizeChange -// result["message"] = fmt.Sprintf("成功: %s (%s→PNG, %dx%d→%dx%d)", -// filepath.Base(inputPath), format, origSize.X, origSize.Y, newSize.X, newSize.Y) -// -// return result -//} -// -//// BatchPNGConverter 批量PNG转换器 -//type BatchPNGConverter struct { -// converter *ImageToPNGConverter -// outputDir string -// statistics map[string]interface{} -// mu sync.Mutex -//} -// -//// NewBatchPNGConverter 创建批量转换器 -//func NewBatchPNGConverter(converter *ImageToPNGConverter, outputDir string) *BatchPNGConverter { -// os.MkdirAll(outputDir, 0755) -// return &BatchPNGConverter{ -// converter: converter, -// outputDir: outputDir, -// statistics: make(map[string]interface{}), -// } -//} -// -//// GetOutputPath 生成输出路径 -//func (b *BatchPNGConverter) GetOutputPath(inputPath, suffix string) string { -// baseName := filepath.Base(inputPath) -// ext := filepath.Ext(baseName) -// nameWithoutExt := strings.TrimSuffix(baseName, ext) -// -// outputFilename := nameWithoutExt + suffix + ".png" -// return filepath.Join(b.outputDir, outputFilename) -//} -// -//// ProcessSingle 处理单张图片 -//func (b *BatchPNGConverter) ProcessSingle(inputPath, outputPath, suffix string) map[string]interface{} { -// if outputPath == "" { -// outputPath = b.GetOutputPath(inputPath, suffix) -// } -// -// // 确保输出目录存在 -// os.MkdirAll(filepath.Dir(outputPath), 0755) -// -// return b.converter.ProcessImageFile(inputPath, outputPath) -//} -// -//// ProcessBatch 批量处理图片 -//func (b *BatchPNGConverter) ProcessBatch(inputPaths []string, suffix string, maxWorkers int) map[string]interface{} { -// startTime := time.Now() -// -// stats := map[string]interface{}{ -// "total": len(inputPaths), -// "success": 0, -// "failed": 0, -// "total_size_reduction": 0.0, -// "total_file_size_orig": int64(0), -// "total_file_size_new": int64(0), -// "results": []map[string]interface{}{}, -// } -// -// // 使用工作池 -// var wg sync.WaitGroup -// semaphore := make(chan struct{}, maxWorkers) -// resultsChan := make(chan map[string]interface{}, len(inputPaths)) -// -// for _, inputPath := range inputPaths { -// wg.Add(1) -// go func(path string) { -// defer wg.Done() -// semaphore <- struct{}{} -// defer func() { <-semaphore }() -// -// outputPath := b.GetOutputPath(path, suffix) -// result := b.ProcessSingle(path, outputPath, suffix) -// resultsChan <- result -// }(inputPath) -// } -// -// // 收集结果 -// go func() { -// wg.Wait() -// close(resultsChan) -// }() -// -// completed := 0 -// for result := range resultsChan { -// completed++ -// b.mu.Lock() -// stats["results"] = append(stats["results"].([]map[string]interface{}), result) -// -// if result["success"].(bool) { -// stats["success"] = stats["success"].(int) + 1 -// stats["total_size_reduction"] = stats["total_size_reduction"].(float64) + result["size_reduction"].(float64) -// stats["total_file_size_orig"] = stats["total_file_size_orig"].(int64) + result["orig_file_size"].(int64) -// stats["total_file_size_new"] = stats["total_file_size_new"].(int64) + result["new_file_size"].(int64) -// } else { -// stats["failed"] = stats["failed"].(int) + 1 -// } -// b.mu.Unlock() -// -// fmt.Printf("[%d/%d] %s\n", completed, len(inputPaths), result["message"]) -// } -// -// // 计算统计信息 -// stats["elapsed_time"] = time.Since(startTime).Seconds() -// if stats["success"].(int) > 0 { -// stats["avg_size_reduction"] = stats["total_size_reduction"].(float64) / float64(stats["success"].(int)) -// if stats["total_file_size_orig"].(int64) > 0 { -// stats["total_file_size_change"] = float64(stats["total_file_size_new"].(int64)) / float64(stats["total_file_size_orig"].(int64)) -// } else { -// stats["total_file_size_change"] = 1.0 -// } -// } -// -// return stats -//} -// -//// FindImageFiles 查找目录中的图片文件 -//func FindImageFiles(directory string, recursive bool) []string { -// supportedExtensions := map[string]bool{ -// ".jpg": true, -// ".jpeg": true, -// ".png": true, -// ".gif": true, -// ".bmp": true, -// ".tif": true, -// ".tiff": true, -// ".webp": true, -// ".jfif": true, -// ".ico": true, -// ".ppm": true, -// ".pgm": true, -// ".pbm": true, -// ".pnm": true, -// } -// -// var imagePaths []string -// -// if recursive { -// filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { -// if err != nil { -// return err -// } -// if !info.IsDir() { -// ext := strings.ToLower(filepath.Ext(path)) -// if supportedExtensions[ext] { -// imagePaths = append(imagePaths, path) -// } -// } -// return nil -// }) -// } else { -// files, err := ioutil.ReadDir(directory) -// if err != nil { -// return imagePaths -// } -// -// for _, file := range files { -// if !file.IsDir() { -// ext := strings.ToLower(filepath.Ext(file.Name())) -// if supportedExtensions[ext] { -// imagePaths = append(imagePaths, filepath.Join(directory, file.Name())) -// } -// } -// } -// } -// -// return imagePaths -//} -// -//// PrintBanner 打印程序标题 -//func PrintBanner() { -// banner := ` -//╔══════════════════════════════════════════════════╗ -//║ 图片去白边转PNG工具 v1.0 ║ -//║ Image White Border Removal & PNG Converter ║ -//╚══════════════════════════════════════════════════╝ -//` -// fmt.Println(banner) -//} -// -//// PrintSummary 打印处理总结 -//func PrintSummary(stats map[string]interface{}) { -// fmt.Println("\n" + strings.Repeat("=", 60)) -// fmt.Println("📊 处理总结") -// fmt.Println(strings.Repeat("=", 60)) -// fmt.Printf("📁 总共处理: %d 张图片\n", stats["total"]) -// fmt.Printf("✅ 成功: %d 张\n", stats["success"]) -// fmt.Printf("❌ 失败: %d 张\n", stats["failed"]) -// -// if stats["success"].(int) > 0 { -// fmt.Printf("⏱️ 耗时: %.2f 秒\n", stats["elapsed_time"].(float64)) -// -// if avgReduction, ok := stats["avg_size_reduction"]; ok { -// fmt.Printf("📐 平均尺寸减少: %.1f%%\n", avgReduction.(float64)*100) -// } -// -// if change, ok := stats["total_file_size_change"]; ok { -// changeVal := change.(float64) -// if changeVal < 1 { -// fmt.Printf("💾 总文件大小减少: %.1f%%\n", (1-changeVal)*100) -// } else if changeVal > 1 { -// fmt.Printf("💾 总文件大小增加: %.1f%%\n", (changeVal-1)*100) -// } else { -// fmt.Println("💾 总文件大小基本不变") -// } -// } -// } -// fmt.Println(strings.Repeat("=", 60)) -//} -// -//// 辅助函数 -//func absDiff(a, b uint8) uint8 { -// if a > b { -// return a - b -// } -// return b - a -//} -// -//func max(a, b int) int { -// if a > b { -// return a -// } -// return b -//} -// -//func min(a, b int) int { -// if a < b { -// return a -// } -// return b -//} -// -//func main() { -// // 直接设置参数值,不需要命令行输入 -// -// // ============ 参数配置区 ============ -// // 基础参数 -// inputPath := "D:\\isbn_images\\result\\matched\\9771671688095.jpg" // 输入文件或目录路径 -// outputPath := "D:\\isbn_images\\result\\matched\\output.png" // 输出文件路径(单文件模式) -// outputDir := "D:\\isbn_images\\result\\matched\\" // 输出目录路径(批量模式) -// suffix := "_trimmed" // 输出文件名后缀 -// -// // 处理参数 -// threshold := 240 // 背景检测阈值 (0-255) -// margin := 0 // 保留边距像素 -// transparent := false // 保持透明背景 -// compressLevel := 6 // PNG压缩级别 (0-9) -// -// // 批量处理参数 -// recursive := false // 递归处理子目录 -// jobs := 4 // 并行处理数 -// force := true // 覆盖已存在的输出文件(设置为true不询问) -// verbose := true // 显示详细处理信息 -// showBanner := true // 显示标题横幅 -// // ============ 参数配置结束 ============ -// -// if showBanner { -// PrintBanner() -// } -// -// // 创建转换器 -// compressionLevel := png.DefaultCompression -// switch { -// case compressLevel <= 0: -// compressionLevel = png.NoCompression -// case compressLevel >= 9: -// compressionLevel = png.BestCompression -// default: -// // 使用默认压缩级别 -// } -// -// converter := NewImageToPNGConverter( -// threshold, -// margin, -// &color.RGBA{R: 255, G: 255, B: 255, A: 255}, -// nil, -// transparent, -// compressionLevel, -// 95, -// ) -// -// batchConverter := NewBatchPNGConverter(converter, outputDir) -// -// // 检查输入路径 -// info, err := os.Stat(inputPath) -// if err != nil { -// fmt.Printf("❌ 错误: 路径不存在 - %s\n", inputPath) -// return -// } -// -// if !info.IsDir() { -// // 单文件模式 -// fmt.Printf("📄 处理单文件: %s\n", inputPath) -// -// // 如果outputPath为空,则生成默认输出路径 -// if outputPath == "" { -// outputPath = batchConverter.GetOutputPath(inputPath, suffix) -// } -// -// // 检查输出文件是否存在,如果force为false则询问 -// if _, err := os.Stat(outputPath); err == nil && !force { -// fmt.Printf("⚠️ 警告: 输出文件已存在 - %s\n", outputPath) -// fmt.Println("已设置force=true,直接覆盖") -// } -// -// result := batchConverter.ProcessSingle(inputPath, outputPath, suffix) -// -// if result["success"].(bool) { -// fmt.Printf("\n✅ %s\n", result["message"]) -// fmt.Printf("💾 输出文件: %s\n", result["output_path"]) -// -// if reduction, ok := result["size_reduction"]; ok { -// reductionVal := reduction.(float64) -// if reductionVal > 0 { -// fmt.Printf("📐 尺寸减少: %.1f%%\n", reductionVal*100) -// } else if reductionVal < 0 { -// fmt.Printf("📐 尺寸增加: %.1f%%\n", -reductionVal*100) -// } -// } -// -// if change, ok := result["file_size_change"]; ok { -// changeVal := change.(float64) -// if changeVal < 1 { -// fmt.Printf("💿 文件大小减少: %.1f%%\n", (1-changeVal)*100) -// } else if changeVal > 1 { -// fmt.Printf("💿 文件大小增加: %.1f%%\n", (changeVal-1)*100) -// } -// } -// } else { -// fmt.Printf("\n❌ %s\n", result["message"]) -// } -// } else { -// // 批量模式 -// fmt.Printf("📁 扫描目录: %s\n", inputPath) -// imageFiles := FindImageFiles(inputPath, recursive) -// -// if len(imageFiles) == 0 { -// fmt.Println("未找到支持的图片文件") -// return -// } -// -// fmt.Printf("找到 %d 张图片\n", len(imageFiles)) -// -// // 检查输出目录,如果force为false则询问 -// if _, err := os.Stat(outputDir); err == nil && !force { -// files, _ := ioutil.ReadDir(outputDir) -// if len(files) > 0 { -// fmt.Printf("⚠️ 警告: 输出目录不为空 - %s\n", outputDir) -// fmt.Println("已设置force=true,直接继续处理") -// } -// } -// -// fmt.Printf("📂 输出目录: %s\n", outputDir) -// fmt.Printf("⚡ 并行处理: %d 个线程\n", jobs) -// fmt.Println(strings.Repeat("-", 60)) -// -// // 批量处理 -// stats := batchConverter.ProcessBatch(imageFiles, suffix, jobs) -// -// // 打印总结 -// PrintSummary(stats) -// -// // 显示失败详情 -// if stats["failed"].(int) > 0 && verbose { -// fmt.Println("\n❌ 失败详情:") -// for _, result := range stats["results"].([]map[string]interface{}) { -// if !result["success"].(bool) { -// fmt.Printf(" %s: %s\n", -// filepath.Base(result["input_path"].(string)), -// result["error"]) -// } -// } -// } -// } -//} diff --git a/kongfz/dll/kongfz.dll b/kongfz/dll/kongfz.dll index 028fdc3..4ccdeea 100644 Binary files a/kongfz/dll/kongfz.dll and b/kongfz/dll/kongfz.dll differ diff --git a/kongfz/dll/kongfz.h b/kongfz/dll/kongfz.h index 263dd86..0d69b37 100644 --- a/kongfz/dll/kongfz.h +++ b/kongfz/dll/kongfz.h @@ -88,55 +88,67 @@ extern "C" { #endif -// 孔网登录 +// OutLogin 孔网登录 // extern __declspec(dllexport) char* OutLogin(char* username, char* password); -// 获取孔网用户信息 +// OutGetUserMsg 获取孔网用户信息 // extern __declspec(dllexport) char* OutGetUserMsg(char* token); -// 获取商品模版--已登的店铺 +// OutGetGoodsTplMsg 获取商品模版--已登的店铺 // extern __declspec(dllexport) char* OutGetGoodsTplMsg(char* token, char* proxy, char* itemId); -// 获取商品列表-已登的店铺 +// OutGetGoodsListMsgFromSelfShop 获取商品列表-已登的店铺 // extern __declspec(dllexport) char* OutGetGoodsListMsgFromSelfShop(char* token, char* proxy, char* itemSn, char* priceMin, char* priceMax, char* startCreateTime, char* endCreateTime, char* requestType, int isItemSnEqual, int page, int size); -// 新增商品-已登的店铺(带有Out的都非官方标准接口) +// OutAddGoods 新增商品-已登的店铺 // extern __declspec(dllexport) char* OutAddGoods(char* token, char* proxy, char* formData); -// 删除商品-已登的店铺 +// OutDelGoodsFromSelfShop 删除商品-已登的店铺 // extern __declspec(dllexport) char* OutDelGoodsFromSelfShop(char* token, char* proxy, char* itemId); -// 获取孔网商品图片和信息(官图和拍图)-带有店铺过滤 +// OutGetImageFilterShopId 获取孔网商品图片和信息(官图和拍图)-带有店铺过滤 // extern __declspec(dllexport) char* OutGetImageFilterShopId(char* token, char* proxy, char* isbn, int shopId, int isLiveImage, int isReturnMsg); -// 获取孔网商品图片和信息(官图和拍图) +// OutGetImageByIsbn 获取孔网商品图片和信息(官图和拍图) // -extern __declspec(dllexport) char* OutGetImageByIsbn(char* token, char* isbn, char* proxy, int isLiveImage, int isReturnMsg); +extern __declspec(dllexport) char* OutGetImageByIsbn(char* token, char* proxy, char* isbn, int isLiveImage, int isReturnMsg); -// 获取商品列表通过店铺ID +// OutGetGoodsListMsgByShopId 获取商品列表通过店铺ID // extern __declspec(dllexport) char* OutGetGoodsListMsgByShopId(int shopId, char* proxy, int retPrice, int isImage, char* sortType, char* sort, float priceMin, float priceMax, int pageNum, int returnNum); -// 获取商品信息通过商品详情链接 +// OutGetGoodsMsgByDetailUrl 获取商品信息通过商品详情链接 // extern __declspec(dllexport) char* OutGetGoodsMsgByDetailUrl(char* detailUrl, char* proxy); -// 获取销量榜商品列表 +// OutGetTopGoodsListMsg 获取销量榜商品列表 // extern __declspec(dllexport) char* OutGetTopGoodsListMsg(int catId, char* proxy); -// 初始化配置 +// KongfzDeliveryMethodList 获取配送方式列表 +// +extern __declspec(dllexport) char* KongfzDeliveryMethodList(int appId, char* appSecret, char* accessToken); + +// KongfzOrderDeliver 订单发货 +// +extern __declspec(dllexport) char* KongfzOrderDeliver(int appId, char* appSecret, char* accessToken, int orderId, char* shippingId, char* shippingCom, char* shipmentNum, char* userDefined, char* moreShipmentNum); + +// KongfzOrderSynchronization 孔网订单同步 +// +extern __declspec(dllexport) char* KongfzOrderSynchronization(int appId, char* appSecret, char* accessToken, char* shippingComName, int orderId, char* shippingId, char* shippingCom, char* shipmentNum, char* userDefined, char* moreShipmentNum); + +// Initialize 初始化配置 // extern __declspec(dllexport) char* Initialize(char* configJSON); -// 释放C字符串内存 +// FreeCString 释放C字符串内存 // extern __declspec(dllexport) void FreeCString(char* str); diff --git a/kongfz/kongfz.go b/kongfz/kongfz.go index 2e5bcfe..bdde7ba 100644 --- a/kongfz/kongfz.go +++ b/kongfz/kongfz.go @@ -5,6 +5,7 @@ package main */ import "C" import ( + "crypto/md5" "encoding/json" "fmt" "io" @@ -13,6 +14,7 @@ import ( "net/http" "net/url" "regexp" + "sort" "strconv" "strings" "time" @@ -23,27 +25,28 @@ import ( "github.com/parnurzeal/gorequest" ) +// Config 结构体定义应用程序配置 type Config struct { App struct { - MaxRetryTimes int `ini:"app.max_retry_times" json:"max_retry_times" default:"3"` - RateLimitDelay time.Duration `ini:"app.rate_limit_delay" json:"rate_limit_delay" default:"500ms"` - Size int `ini:"app.size" json:"size" default:"5"` - DefaultUserAgent string `ini:"app.default_user_agent" json:"default_user_agent" default:"Mozilla/5.0"` + MaxRetryTimes int `ini:"app.max_retry_times" json:"max_retry_times" default:"3"` // 最大重试次数 + RateLimitDelay time.Duration `ini:"app.rate_limit_delay" json:"rate_limit_delay" default:"500ms"` // 请求延迟 + Size int `ini:"app.size" json:"size" default:"5"` // 默认大小 + DefaultUserAgent string `ini:"app.default_user_agent" json:"default_user_agent" default:"Mozilla/5.0"` // 默认 User-Agent } `ini:"app" json:"app"` API struct { - LoginURL string `ini:"api.login_url" json:"login_url" default:"https://login.kongfz.com/Pc/Login/account"` - BookSearchURL string `ini:"api.book_search_url" json:"book_search_url" default:"https://search.kongfz.com/pc-gw/search-web/client/pc/bookLib/keyword/list"` - ProductSearchURL string `ini:"api.product_search_url" json:"product_search_url" default:"https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list"` + LoginURL string `ini:"api.login_url" json:"login_url" default:"https://login.kongfz.com/Pc/Login/account"` // 登录 URL + BookSearchURL string `ini:"api.book_search_url" json:"book_search_url" default:"https://search.kongfz.com/pc-gw/search-web/client/pc/bookLib/keyword/list"` // 图书搜索 URL + ProductSearchURL string `ini:"api.product_search_url" json:"product_search_url" default:"https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list"` // 商品搜索 URL } `ini:"api" json:"api"` Proxy struct { - Servers string `ini:"proxy.servers" json:"servers" default:"http-dynamic.xiaoxiangdaili.com,http-dynamic-S02.xiaoxiangdaili.com,http-dynamic-S03.xiaoxiangdaili.com,http-dynamic-S04.xiaoxiangdaili.com"` - Username string `ini:"proxy.username" json:"username" default:"1297757178467602432"` - Password string `ini:"proxy.password" json:"password" default:"QgQBvP7f"` - TailMachineCode string `ini:"proxy.tail_machine_code" json:"tail_machine_code" default:"b7bf22a237ec692f13fcc2c43ee63252"` - TailCardKey string `ini:"proxy.tail_card_key" json:"tail_card_key" default:"DL_20_YK_1920acb2129844c2aabade3896560a9b"` - ProxyFilePath string `ini:"proxy.proxy_file_path" json:"proxy_file_path" default:"dll/proxyConfig.dll"` + Servers string `ini:"proxy.servers" json:"servers" default:"http-dynamic.xiaoxiangdaili.com,http-dynamic-S02.xiaoxiangdaili.com,http-dynamic-S03.xiaoxiangdaili.com,http-dynamic-S04.xiaoxiangdaili.com"` // 代理服务器列表 + Username string `ini:"proxy.username" json:"username" default:"1297757178467602432"` // 代理用户名 + Password string `ini:"proxy.password" json:"password" default:"QgQBvP7f"` // 代理密码 + TailMachineCode string `ini:"proxy.tail_machine_code" json:"tail_machine_code" default:"b7bf22a237ec692f13fcc2c43ee63252"` // 尾机编码 + TailCardKey string `ini:"proxy.tail_card_key" json:"tail_card_key" default:"DL_20_YK_1920acb2129844c2aabade3896560a9b"` // 尾卡密钥 + ProxyFilePath string `ini:"proxy.proxy_file_path" json:"proxy_file_path" default:"dll/proxyConfig.dll"` // 代理配置文件路径 } `ini:"proxy" json:"proxy"` } @@ -83,135 +86,153 @@ type BookInfo struct { DetailUrl string `json:"detail_url"` // 商品详情url } -// APIResponse 响应结构体 +// APIResponse 通用API响应结构体 type APIResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Data interface{} `json:"data,omitempty"` + Success bool `json:"success"` // 请求是否成功 + Message string `json:"message,omitempty"` // 错误信息(成功时可选) + Data interface{} `json:"data,omitempty"` // 响应数据(失败时可选) } -// 孔网用户信息响应结构体 +// UserInfo 孔网用户信息结构体 type UserInfo struct { - UserID int64 `json:"userId"` - Nickname string `json:"nickname"` - Mobile string `json:"mobile"` + UserID int64 `json:"userId"` // 用户ID + Nickname string `json:"nickname"` // 用户昵称 + Mobile string `json:"mobile"` // 手机号 } -// 全局变量 +// 全局配置变量 var ( - cf Config // 配置信息 + cf Config // 存储应用程序配置 ) -// ProductInfo 商品信息结构 +// ProductInfo 商品信息结构体 type ProductInfo struct { - ItemID string `json:"itemId"` - BookName string `json:"bookName"` - Price string `json:"price"` - ShippingFee string `json:"shippingFee"` + ItemID string `json:"itemId"` // 商品ID + BookName string `json:"bookName"` // 书名 + Price string `json:"price"` // 价格 + ShippingFee string `json:"shippingFee"` // 运费 } -// 图书详情响应结构体 +// BookDetailResponse 图书详情响应结构体 type BookDetailResponse struct { - Status bool `json:"status"` - Result BookList `json:"result"` - ErrMessage string `json:"errMessage"` - ErrCode int `json:"errCode"` + Status bool `json:"status"` // 状态 + Result BookList `json:"result"` // 结果数据 + ErrMessage string `json:"errMessage"` // 错误信息 + ErrCode int `json:"errCode"` // 错误码 } -// 图书列表结构体 +// BookList 图书列表结构体 type BookList struct { - Current int `json:"current"` - Data []BookInformation `json:"data"` - Total int `json:"total"` + Current int `json:"current"` // 当前页码 + Data []BookInformation `json:"data"` // 图书数据列表 + Total int `json:"total"` // 总数 } -// 图书信息结构体 +// BookInformation 图书信息结构体 type BookInformation struct { - Author string `json:"author"` - BookName string `json:"bookName"` - ContentIntroduction string `json:"contentIntroduction"` - ImgUrl string `json:"imgUrl"` - Isbn string `json:"isbn"` - ItemUrls ItemUrls `json:"itemUrls"` - Mid int `json:"mid"` - NewMinPrice string `json:"newMinPrice"` - OldMinPrice string `json:"oldMinPrice"` - Press string `json:"press"` - Price string `json:"price"` - PubDate string `json:"pubDate"` - RiseTag string `json:"riseTag"` - AuthorArr []AuthorInfo `json:"authorArr"` - PressUrl string `json:"pressUrl"` + Author string `json:"author"` // 作者 + BookName string `json:"bookName"` // 书名 + ContentIntroduction string `json:"contentIntroduction"` // 内容简介 + ImgUrl string `json:"imgUrl"` // 图片URL + Isbn string `json:"isbn"` // ISBN + ItemUrls ItemUrls `json:"itemUrls"` // 商品URL + Mid int `json:"mid"` // 商家ID + NewMinPrice string `json:"newMinPrice"` // 新书最低价 + OldMinPrice string `json:"oldMinPrice"` // 旧书最低价 + Press string `json:"press"` // 出版社 + Price string `json:"price"` // 价格 + PubDate string `json:"pubDate"` // 出版日期 + RiseTag string `json:"riseTag"` // 上涨标签 + AuthorArr []AuthorInfo `json:"authorArr"` // 作者数组 + PressUrl string `json:"pressUrl"` // 出版社URL } -// 作者信息结构体 +// AuthorInfo 作者信息结构体 type AuthorInfo struct { - Name string `json:"name"` - OriName string `json:"oriName"` - Nationality string `json:"nationality"` - Role string `json:"role"` - Url string `json:"url"` + Name string `json:"name"` // 作者姓名 + OriName string `json:"oriName"` // 原始姓名 + Nationality string `json:"nationality"` // 国籍 + Role string `json:"role"` // 角色 + Url string `json:"url"` // 作者页面URL } -// 商品链接结构体 +// ItemUrls 商品链接结构体 type ItemUrls struct { - AppUrl string `json:"appUrl"` - MUrl string `json:"mUrl"` - MiniUrl string `json:"miniUrl"` - PcUrl string `json:"pcUrl"` + AppUrl string `json:"appUrl"` // App链接 + MUrl string `json:"mUrl"` // 移动端链接 + MiniUrl string `json:"miniUrl"` // 小程序链接 + PcUrl string `json:"pcUrl"` // PC端链接 } -// 店铺快递费参数 +// ParamsInfo 店铺快递费参数结构体 type ParamsInfo struct { - Params []Param `json:"params"` - Area string `json:"area"` + Params []Param `json:"params"` // 参数列表 + Area string `json:"area"` // 地区编码 } +// Param 单个参数结构体 type Param struct { - UserId string `json:"userId"` - ItemId string `json:"itemId"` + UserId string `json:"userId"` // 用户ID + ItemId string `json:"itemId"` // 商品ID } -// 定义快递费用的响应结构体 +// DataItem 快递费用响应数据项 type DataItem struct { - Fee []FeeInfo `json:"fee"` - IsSeller bool `json:"isSeller"` - ItemID string `json:"itemId"` - UserID string `json:"userId"` + Fee []FeeInfo `json:"fee"` // 费用列表 + IsSeller bool `json:"isSeller"` // 是否是卖家 + ItemID string `json:"itemId"` // 商品ID + UserID string `json:"userId"` // 用户ID } +// FeeInfo 费用信息结构体 type FeeInfo struct { - ShippingID string `json:"shippingId"` - ShippingName string `json:"shippingName"` - TotalFee string `json:"totalFee"` - ShippingText string `json:"shippingText"` - FilterTotalFee string `json:"filterTotalFee"` + ShippingID string `json:"shippingId"` // 配送方式ID + ShippingName string `json:"shippingName"` // 配送方式名称 + TotalFee string `json:"totalFee"` // 总费用 + ShippingText string `json:"shippingText"` // 配送说明 + FilterTotalFee string `json:"filterTotalFee"` // 过滤后的总费用 } +// ResponseStruct 快递费响应结构体 type ResponseStruct struct { - Status bool `json:"status"` - Data []DataItem `json:"data"` - Message string `json:"message"` - ErrType string `json:"errType"` + Status bool `json:"status"` // 状态 + Data []DataItem `json:"data"` // 数据 + Message string `json:"message"` // 消息 + ErrType string `json:"errType"` // 错误类型 } -// 获取店铺里商品的快递费(定位到河南) +/* + * 获取店铺里商品的快递费(定位到河南) + * param params[ParamsInfo] 店铺快递费参数结构体 + * param proxy[string] 代理服务器IP + * return 快递费用响应数据项数组,错误信息 + * Error 没有商品信息 + * Error 参数序列化失败 + * Error 请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + */ func getGoodsListShippingFee(params ParamsInfo, proxy string) ([]DataItem, error) { + // 检查参数是否为空 if params.Params == nil { return nil, fmt.Errorf("没有商品信息!") } + // 序列化参数为JSON paramsJSON, err := json.Marshal(params) if err != nil { return nil, fmt.Errorf("参数序列化失败: %v", err) } // URL 编码参数 encodedParams := url.QueryEscape(string(paramsJSON)) + // 构建请求URL url := fmt.Sprintf("https://shop.kongfz.com/book/shopsearch/getShippingFee?params=%s", encodedParams) + // 创建请求对象 request := gorequest.New() + // 设置代理 if proxy != "" { request.Proxy(proxy) } - // 发送请求 + // 发送GET请求 resp, body, errs := request.Get(url). Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). Set("Accept", "application/json, text/plain, */*"). @@ -225,11 +246,13 @@ func getGoodsListShippingFee(params ParamsInfo, proxy string) ([]DataItem, error if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP错误: %s", resp.Status) } + // 解析JSON响应 responseStruct := ResponseStruct{} err = json.Unmarshal([]byte(body), &responseStruct) if err != nil { return nil, fmt.Errorf("解析JSON失败: %v", err) } + // 提取数据项 var dataItem []DataItem for i := range responseStruct.Data { dataItem = append(dataItem, responseStruct.Data[i]) @@ -237,14 +260,26 @@ func getGoodsListShippingFee(params ParamsInfo, proxy string) ([]DataItem, error return dataItem, nil } -// 孔网登录 +/* + * 孔网登录 + * param username[string] 孔网用户名 + * param password[string] 孔网密码 + * return token,错误信息 + * Error 登录请求失败 + * Error 登录失败(HTTP状态码: %d) + * Error 登录成功但未获取到Cookie + * Error 登录失败: 未找到 PHPSESSID + * Error 账号或密码错误 + * Error 登录失败 + * Error 登录失败,未知错误! + */ func outLogin(username, password string) (string, error) { - // 判断用户名和密码是否为空 + // 检查用户名和密码是否为空 if username == "" || password == "" { return "", fmt.Errorf("请输入用户名和密码!") } - // Post请求参数 + // 准备POST请求的表单数据 formData := map[string]string{ "loginName": username, "loginPass": password, @@ -253,7 +288,7 @@ func outLogin(username, password string) (string, error) { // 孔网登录URL loginUrl := "https://login.kongfz.com/Pc/Login/account" - //发送请求 + // 发送登录请求 resp, body, errs := gorequest.New(). Post(loginUrl). Set("Content-Type", "application/x-www-form-urlencoded"). @@ -263,9 +298,11 @@ func outLogin(username, password string) (string, error) { Timeout(15 * time.Second). End() + // 请求错误处理 if len(errs) > 0 { return "", fmt.Errorf("登录请求失败: %v", errs) } + // 检查HTTP状态码 if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("登录失败(HTTP状态码: %d)", resp.StatusCode) @@ -273,6 +310,7 @@ func outLogin(username, password string) (string, error) { // 提取Cookie cookie := resp.Header.Get("Set-Cookie") + // 检查是否登录成功(通过响应内容判断) if strings.Contains(body, "window.location.href='https://login.kongfz.cn/Pc/Session/rsync") { if cookie == "" { return "", fmt.Errorf("登录成功但未获取到Cookie") @@ -307,7 +345,15 @@ func outLogin(username, password string) (string, error) { return "", fmt.Errorf("登录失败,未知错误!") } -// 获取孔网用户信息 +/* + * 获取孔网用户信息 + * param token[string] 孔网token + * return 孔网用户信息结构体,错误信息 + * Error 查询请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + * Error 获取用户失败 + */ func outGetUserMsg(token string) (*UserInfo, error) { // 用户信息URL url := "https://user.kongfz.com/User/Index/getUserInfo/" @@ -344,6 +390,7 @@ func outGetUserMsg(token string) (*UserInfo, error) { return nil, fmt.Errorf("解析JSON失败: %w", err) } + // 创建用户信息对象 user := &UserInfo{} if !userInfo.Status { return nil, fmt.Errorf("获取用户失败!") @@ -354,7 +401,19 @@ func outGetUserMsg(token string) (*UserInfo, error) { return user, nil } -// 获取商品模版-已登的店铺 +/* + * 获取商品模版-已登的店铺 + * param token[string] 孔网token + * param proxy[string] 代理服务器IP + * param itemId[string] 商品ID + * return 商品模板信息结构体,错误信息 + * Error 请先登录获取Token + * Error 代理连接失败 + * Error 查询请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + * Error API返回错误 + */ func outGetGoodsTplMsg(token, proxy, itemId string) (map[string]interface{}, error) { // 判断登录token if token == "" { @@ -380,6 +439,7 @@ func outGetGoodsTplMsg(token, proxy, itemId string) (map[string]interface{}, err Set("Origin", "https://seller.kongfz.com"). Timeout(15 * time.Second). End() + // 错误处理 if errs != nil { // 检查是否是代理相关错误 var isProxyError bool @@ -424,7 +484,27 @@ func outGetGoodsTplMsg(token, proxy, itemId string) (map[string]interface{}, err return nil, fmt.Errorf("API返回错误: %+v", data) } -// 获取商品列表-已登的店铺 +/* + * 获取商品列表-已登的店铺 + * param token[string] 孔网token + * param proxy[string] 代理服务器IP + * param itemSn[string] 货号 + * param priceMin[string] 价格区间-低 + * param priceMax[string] 价格区间-高 + * param startCreateTime[string] 开始时间 + * param endCreateTime[string] 结束时间 + * param requestType[string] 请求类型 + * param isItemSnEqual[int] 商品ID + * param page[int] 页数 + * param size[int] 大小 + * return 商品列表响应信息结构体,错误信息 + * Error 请先登录获取Token + * Error 代理连接失败 + * Error 查询请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + * Error API返回错误 + */ func outGetGoodsListMsgFromSelfShop(token string, proxy string, itemSn string, priceMin string, priceMax string, startCreateTime string, endCreateTime string, requestType string, isItemSnEqual int, page int, size int) (map[string]interface{}, error) { @@ -465,6 +545,7 @@ func outGetGoodsListMsgFromSelfShop(token string, proxy string, itemSn string, p if proxy != "" { request.Proxy(proxy) } + // 发送POST请求 resp, body, errs := request.Post(url). Set("Cookie", fmt.Sprintf("PHPSESSID=%s", token)). Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). @@ -513,7 +594,19 @@ func outGetGoodsListMsgFromSelfShop(token string, proxy string, itemSn string, p return nil, fmt.Errorf("API返回错误: %+v", data) } -// 新增商品-已登的店铺 +/* + * 新增商品-已登的店铺 + * param token[string] 孔网token + * param proxy[string] 代理服务器IP + * param formData[string] 商品信息结构体字符串 + * return 新增商品响应信息结构体,错误信息 + * Error 请先登录获取Token + * Error 代理连接失败 + * Error 查询请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + * Error API返回错误 + */ func outAddGoods(token, proxy, formData string) (map[string]interface{}, error) { // 判断登录token if token == "" { @@ -561,17 +654,31 @@ func outAddGoods(token, proxy, formData string) (map[string]interface{}, error) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP错误: %s", resp.Status) } + // 解析JSON响应 var data map[string]interface{} if err := json.Unmarshal([]byte(body), &data); err != nil { return nil, fmt.Errorf("解析JSON失败: %w", err) } + // 检查响应状态 if val, ok := data["status"]; ok && val == 1 { return data, nil } return nil, fmt.Errorf("API返回错误: %+v", data) } -// 删除商品-已登的店铺 +/* + * 删除商品-已登的店铺 + * param token[string] 孔网token + * param proxy[string] 代理服务器IP + * param itemId[string] 商品ID + * return 删除商品响应信息结构体,错误信息 + * Error 请先登录获取Token + * Error 代理连接失败 + * Error 查询请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + * Error API返回错误 + */ func outDelGoodsFromSelfShop(token, proxy, itemId string) (map[string]interface{}, error) { // 判断登录token if token == "" { @@ -640,8 +747,21 @@ func outDelGoodsFromSelfShop(token, proxy, itemId string) (map[string]interface{ return nil, fmt.Errorf("API返回错误: %+v", data) } -// 获取孔网商品图片(官图和拍图)-带有店铺过滤 +/* + * 获取孔网商品图片(官图和拍图)-带有店铺过滤 + * param token[string] 孔网token + * param proxy[string] 代理服务器IP + * param itemId[string] 商品ID + * return 删除商品响应信息结构体,错误信息 + * Error 请先登录获取Token + * Error 代理连接失败 + * Error 查询请求失败 + * Error HTTP错误 + * Error 解析JSON失败 + * Error API返回错误 + */ func outGetImageFilterShopId(token string, proxy string, isbn string, shopId int, isLiveImage int, isReturnMsg int) (map[string]string, error) { + // 判断是否为官图(isLiveImage=0) if isLiveImage == 0 { // 图书条目URL gtUrl := fmt.Sprintf("%s?keyword=%s", @@ -724,6 +844,7 @@ func outGetImageFilterShopId(token string, proxy string, isbn string, shopId int } return info, nil } + // 处理实拍图(isLiveImage=1) if isLiveImage == 1 { size := 10 // 实拍图 @@ -798,6 +919,7 @@ func outGetImageFilterShopId(token string, proxy string, isbn string, shopId int } else { startIndex = size - 1 } + // 尝试3次获取有效的图片 for attempt := 0; attempt < 3; attempt++ { currentIndex := startIndex - attempt // 检查索引是否有效 @@ -817,6 +939,7 @@ func outGetImageFilterShopId(token string, proxy string, isbn string, shopId int log.Printf("[DEBUG] 索引 %d 的店铺ID需要过滤,跳过", randomNum) continue } + // 返回图片信息 info = map[string]string{ "book_name": item.Title, "book_pic_s": item.ImgUrl, @@ -833,6 +956,7 @@ func outGetImageFilterShopId(token string, proxy string, isbn string, shopId int func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, isReturnMsg int) (*BookInfo, error) { // isLiveImage 1实拍图 0官图 ,isReturnMsg 0商品信息 bookInfo := &BookInfo{} + // 处理官图(isLiveImage=0) if isLiveImage == 0 { // 孔网官图请求 gtUrl := fmt.Sprintf("%s?keyword=%s", @@ -843,7 +967,7 @@ func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, if proxy != "" { requestGt.Proxy(proxy) } - // 发送请求 + // 发送GET请求 respGt, bodyGt, errsGt := requestGt.Get(gtUrl). Set("Cookie", fmt.Sprintf("PHPSESSID=%s", token)). Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). @@ -900,6 +1024,7 @@ func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, } `json:"itemResponse"` } `json:"data"` } + // 解析JSON if err := json.Unmarshal([]byte(bodyGt), &apiGtResp); err != nil { return nil, fmt.Errorf("解析JSON失败: %w", err) } @@ -936,6 +1061,7 @@ func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, } return bookInfo, nil } + // 处理实拍图(isLiveImage=1) if isLiveImage == 1 { size := 10 // 实拍图 @@ -1007,6 +1133,7 @@ func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, } else { startIndex = size - 1 } + // 尝试3次获取有效的图片 for attempt := 0; attempt < 3; attempt++ { currentIndex := startIndex - attempt // 检查索引是否有效 @@ -1014,6 +1141,7 @@ func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, log.Printf("[DEBUG] 索引 %d 超出范围,跳过", currentIndex) continue } + // 随机选择商品 randomNum := rand.Intn(currentIndex + 1) item := apiSptResp.Data.ItemResponse.List[randomNum] // 检查图片URL是否存在 @@ -1021,9 +1149,10 @@ func outGetImageByIsbn(token string, proxy string, isbn string, isLiveImage int, log.Printf("[DEBUG] 索引 %d 的图片URL为空,跳过", randomNum) continue } - // 响应信息 + // 填充图书信息 bookInfo.BookPicS = item.ImgUrl bookInfo.ISBN = item.Isbn + // 如果书名为空,使用商品标题 if bookInfo.BookName == "" { bookInfo.BookName = item.Title if isReturnMsg == 0 { @@ -1123,10 +1252,12 @@ func outGetGoodsListMsgByShopId(shopId int, proxy string, retPrice int, isImage if err != nil { return nil, "", "", err } + // 读取响应体 body, err := io.ReadAll(response.Body) if err != nil { return nil, "", "", err } + // 解析HTML文档 doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body))) // 全部商品数量 num := doc.Find("div.crumbs-nav-main.clearfix").Find("span") @@ -1141,6 +1272,7 @@ func outGetGoodsListMsgByShopId(shopId int, proxy string, retPrice int, isImage } else { log.Printf("未找到页数!") } + // 提取商品信息 infoDiv := doc.Find("div.list-content") var params ParamsInfo if infoDiv.Length() > 0 { @@ -1170,12 +1302,14 @@ func outGetGoodsListMsgByShopId(shopId int, proxy string, retPrice int, isImage } books = append(books, book) } + // 如果需要查询快递费 if params.Params != nil { params.Area = "13003000000" dataItem, err := getGoodsListShippingFee(params, proxy) if err != nil { return nil, "", "", err } + // 将快递费信息填充到图书信息中 for i := 0; i < len(books); i++ { itemId := fmt.Sprintf("%d", books[i].ItemId) for _, data := range dataItem { @@ -1191,10 +1325,12 @@ func outGetGoodsListMsgByShopId(shopId int, proxy string, retPrice int, isImage // 获取商品信息通过商品详情链接 func outGetGoodsMsgByDetailUrl(detailUrl, proxy string) (*BookInfo, error) { + // 发送请求获取响应 response, err := fetchResponse(detailUrl, proxy) if err != nil { return nil, err } + // 读取响应体 body, err := io.ReadAll(response.Body) if err != nil { return nil, err @@ -1210,9 +1346,9 @@ func outGetGoodsMsgByDetailUrl(detailUrl, proxy string) (*BookInfo, error) { return nil, err } book := BookInfo{} - //书名 + // 书名 book.BookName = strings.TrimSpace(document.Find("h1.title").Text()) - //作者等信息 + // 提取作者、出版社等信息 topDiv := document.Find("div.keywords-define.keywords-define-1000.clear-fix") if topDiv.Length() > 0 { topDiv.Find("li").Each(func(i int, li *goquery.Selection) { @@ -1224,6 +1360,7 @@ func outGetGoodsMsgByDetailUrl(detailUrl, proxy string) (*BookInfo, error) { titleText := strings.TrimSpace(titleSpan.Text()) contentText := strings.TrimSpace(contentSpan.Text()) titleText = strings.TrimSpace(titleText) + // 根据标题字段填充对应的图书信息字段 if strings.Contains(titleText, "作者") { book.Author = cleanString(contentText) } @@ -1271,6 +1408,7 @@ func outGetGoodsMsgByDetailUrl(detailUrl, proxy string) (*BookInfo, error) { } }) } else { + // 备选提取方式 botDiv := document.Find("div.detail-lists.clear-fix") botDiv.Find("li").Each(func(i int, li *goquery.Selection) { spanText := strings.TrimSpace(li.Find("span").Text()) @@ -1310,7 +1448,7 @@ func outGetGoodsMsgByDetailUrl(detailUrl, proxy string) (*BookInfo, error) { }) } - //图片 + // 提取商品图片 var imgUrls []string tpUl := document.Find("ul.lg-list") tpUl.Find("img").Each(func(i int, s *goquery.Selection) { @@ -1320,45 +1458,49 @@ func outGetGoodsMsgByDetailUrl(detailUrl, proxy string) (*BookInfo, error) { } }) book.BookPicS = strings.Join(imgUrls, ",") - // 售价 + // 提取售价 price := document.Find("i.now-price-text").Text() priceN := regexp.MustCompile(`(\d+\.?\d*)`) if match := priceN.FindStringSubmatch(price); len(match) > 0 { book.SellingPrice = match[1] } - // 定价价 + // 提取定价 fixPrice := document.Find("span.origin-price-text.clearfix").Text() fixPriceN := regexp.MustCompile(`(\d+\.?\d*)`) if match := fixPriceN.FindStringSubmatch(fixPrice); len(match) > 0 { book.FixPrice = match[1] } - // 品相 + // 提取品相 text := document.Find("span.quality-text-cot.clearfix i").Text() book.Condition = strings.TrimSpace(text) - // 快递费 + // 设置快递费 book.ExpressDeliveryFee = fee return &book, nil } // 获取商品信息的快递费(定位到河南) func getBookDetailShippingFee(url, proxy string) (string, error) { + // 从URL中提取店铺ID和商品ID compile := regexp.MustCompile(`kongfz\.com/(\d+)/(\d+)`) match := compile.FindStringSubmatch(url) var shippingFee string var shopId int var itemId int if len(match) == 3 { - firstNum, err := strconv.Atoi(match[1]) // 店铺ID + // 提取店铺ID + firstNum, err := strconv.Atoi(match[1]) if err != nil { return "", fmt.Errorf("无效的店铺编码: %s", match[1]) } shopId = firstNum - secondNum, err := strconv.Atoi(match[2]) // 图书ID + // 提取商品ID + secondNum, err := strconv.Atoi(match[2]) if err != nil { return "", fmt.Errorf("无效的图书编码: %s", match[2]) } itemId = secondNum } + // 构建快递费查询URL shippingFeeUrl := fmt.Sprintf("https://book.kongfz.com/store-web/pc/v1/mould/calculateFee?area=13003000000&itemId=%d&shopId=%d", itemId, shopId) // 创建HTTP客户端 request := gorequest.New() @@ -1396,11 +1538,12 @@ func getBookDetailShippingFee(url, proxy string) (string, error) { } `json:"result"` Status bool `json:"status"` }{} - + // 解析JSON err := json.Unmarshal([]byte(body), &calculateFee) if err != nil { return "", fmt.Errorf("解析JSON失败: %v", err) } + // 提取快递费 for _, fee := range calculateFee.Result.FeeList { shippingFee = fee.ShippingValue } @@ -1413,6 +1556,7 @@ func fetchResponse(url, proxy string) (*http.Response, error) { maxRetries := 3 var detailsResp *http.Response var errors []error + // 重试机制 for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { log.Printf("第 %d 次重试请求...", attempt) @@ -1421,6 +1565,7 @@ func fetchResponse(url, proxy string) (*http.Response, error) { log.Printf("等待 %v 后重试", waitTime) time.Sleep(waitTime) } + // 根据是否有代理发送请求 if proxy != "" { detailsResp, _, errors = gorequest.New(). Get(url). @@ -1473,6 +1618,7 @@ func fetchResponse(url, proxy string) (*http.Response, error) { var proxyAuthFailed bool var timeoutError bool var connectionError bool + // 分析错误类型 for _, e := range errors { errStr := e.Error() if strings.Contains(errStr, "Proxy Authentication Required") { @@ -1485,17 +1631,21 @@ func fetchResponse(url, proxy string) (*http.Response, error) { connectionError = true } } + // 根据错误类型返回相应的错误信息 if proxyAuthFailed { return nil, fmt.Errorf("代理认证失败") } + if timeoutError { return nil, fmt.Errorf("请求超时,经过 %d 次尝试,超时网址:%s", maxRetries+1, url) } + if connectionError { return nil, fmt.Errorf("网络连接错误,经过 %d 次尝试,错误网址:%s", maxRetries+1, url) } return nil, fmt.Errorf("查询请求失败,经过 %d 次尝试: %v,失败网址:%s", maxRetries+1, errors, url) } + // 检查HTTP状态码 if detailsResp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP错误: %s", detailsResp.Status) } @@ -1514,7 +1664,7 @@ func outGetTopGoodsListMsg(catId int, proxy string) ([]string, error) { } // 设置超时和其他配置 request.Timeout(30 * time.Second) - // 发送请求 + // 发送GET请求 resp, body, errs := request.Get(url). Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). Set("Accept", "application/json, text/plain, */*"). @@ -1529,21 +1679,26 @@ func outGetTopGoodsListMsg(catId int, proxy string) ([]string, error) { if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP错误: %s", resp.Status) } + // 解析响应 var bookDetailResponse BookDetailResponse err := json.Unmarshal([]byte(body), &bookDetailResponse) if err != nil { return nil, fmt.Errorf("解析JSON失败: %v", err) } + + // 检查响应状态 if !bookDetailResponse.Status { return nil, fmt.Errorf("API返回错误: %s (代码: %d)", bookDetailResponse.ErrMessage, bookDetailResponse.ErrCode) } + // 提取ISBN列表 var isbnList []string for _, item := range bookDetailResponse.Result.Data { if item.Isbn != "" { isbnList = append(isbnList, item.Isbn) } } + // 去除重复的ISBN isbnList = removeDuplicateISBNs(isbnList) return isbnList, nil } @@ -1561,7 +1716,295 @@ func removeDuplicateISBNs(isbns []string) []string { return result } +// generateSign 生成签名 +func generateSign(params map[string]interface{}, appSecret string) string { + // 获取所有键并排序 + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + // 拼接签名字符串 + var signStr strings.Builder + for _, k := range keys { + // 跳过 sign 参数 + if strings.ToLower(k) == "sign" { + continue + } + // 获取参数值 + value := "" + if params[k] != nil { + value = fmt.Sprintf("%v", params[k]) + } + // 按照文档格式:参数名+参数值 + signStr.WriteString(k + value) + } + + // 根据签名方法生成签名 + signMethod := "md5" + if method, ok := params["signMethod"].(string); ok { + signMethod = strings.ToLower(method) + } + + signString := signStr.String() + + // 使用MD5算法生成签名 + if signMethod == "md5" { + // MD5算法:md5(appSecret + signString + appSecret) + data := appSecret + signString + appSecret + hash := md5.Sum([]byte(data)) + result := strings.ToUpper(fmt.Sprintf("%x", hash)) + fmt.Printf("Debug: MD5签名结果: %s\n", result) + return result + } + + return "" +} + +// KwAPIResponse 孔网API响应结构体 +type KwAPIResponse struct { + ErrorResponse *ErrorResponse `json:"errorResponse"` // 错误响应 + SuccessResponse []ShippingMethod `json:"successResponse"` // 成功响应 + RequestId string `json:"requestId"` // 请求ID + RequestMethod string `json:"requestMethod"` // 请求方法 +} + +type ErrorResponse struct { + Code int `json:"code"` // 错误码 + Msg string `json:"msg"` // 错误信息 + SubCode *int `json:"subCode"` // 使用指针类型,因为可能是null + SubMsg *string `json:"subMsg"` // 使用指针类型,因为可能是null +} + +type ShippingMethod struct { + ShippingId string `json:"shippingId"` // 配送方式ID + ShippingName string `json:"shippingName"` // 配送方式名称 + IsDefault bool `json:"isDefault"` // 是否默认配送方式 + Companies []Company `json:"companies"` // 快递公司列表 +} + +type Company struct { + ShippingCom string `json:"shippingCom"` // 快递公司代号 + ShippingComName string `json:"shippingComName"` // 快递公司名称 + IsDefault bool `json:"isDefault"` // 是否默认 +} + +// 获取配送方式列表 +func kongfzDeliveryMethodList(appId int, appSecret, accessToken string) (string, error) { + kUrl := fmt.Sprint("https://open.kongfz.com/router/rest") + dateTime := getCurrentTimeGMT8() + + // 生成签名参数 + params := map[string]interface{}{ + "method": "kongfz.delivery.method.list", + "appId": appId, + "accessToken": accessToken, + "datetime": dateTime, + "format": "json", + "v": "1.0", + "signMethod": "md5", + "simplify": 0, + } + // 生成签名 + sign := generateSign(params, appSecret) + + // 构建请求体 + formData := map[string]interface{}{ + "method": "kongfz.delivery.method.list", + "appId": appId, + "accessToken": accessToken, + "datetime": dateTime, + "format": "json", + "v": "1.0", + "signMethod": "md5", + "sign": sign, + "simplify": 0, + } + + // 发送POST请求 + request := gorequest.New() + resp, body, errs := request.Post(kUrl). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "*/*"). + Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"). + Type("form"). // 关键:明确指定为表单格式 + Timeout(30 * time.Second). + Send(formData). + End() + // 错误处理 + if len(errs) > 0 { + return "", fmt.Errorf("登录请求失败: %v", errs) + } + // 检查HTTP状态码 + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + + // 异常处理 + if response["ErrorResponse"] != nil { + var kwAPIResponse KwAPIResponse + if err := json.Unmarshal([]byte(body), &kwAPIResponse); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", kwAPIResponse.ErrorResponse.Msg, kwAPIResponse.ErrorResponse.Code) + } + + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// 订单发货 +func kongfzOrderDeliver(appId int, appSecret, accessToken string, + orderId int, shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum string) (string, error) { + kUrl := fmt.Sprint("https://open.kongfz.com/router/rest") + dateTime := getCurrentTimeGMT8() + // 参数验证 + if shippingId != "noLogistics" { + if shippingCom == "" { + return "", fmt.Errorf("当 shippingId 不等于 noLogistics 时,shippingCom 参数为必填。shippingId是: %v", shippingId) + } + if shipmentNum == "" { + return "", fmt.Errorf("当 shippingId 不等于 noLogistics 时, shipmentNum 参数为必填。shippingId是: %v", shippingId) + } + } + if shipmentNum == "other" { + if userDefined == "" { + return "", fmt.Errorf("当 shippingCom 等于 other 时, userDefined 参数为必填。shipmentNum是: %v", shipmentNum) + } + } + + // 生成签名参数 + params := map[string]interface{}{ + "method": "kongfz.order.deliver", + "appId": appId, + "accessToken": accessToken, + "datetime": dateTime, + "format": "json", + "v": "1.0", + "signMethod": "md5", + "simplify": 0, + "orderId": orderId, + "shippingId": shippingId, + "shippingCom": shippingCom, + "shipmentNum": shipmentNum, + "userDefined": userDefined, + "moreShipmentNum": moreShipmentNum, + } + // 生成签名 + sign := generateSign(params, appSecret) + + // 构建请求体 + formData := map[string]interface{}{ + "method": "kongfz.order.deliver", + "appId": appId, + "accessToken": accessToken, + "datetime": dateTime, + "format": "json", + "v": "1.0", + "signMethod": "md5", + "sign": sign, + "simplify": 0, + "orderId": orderId, + "shippingId": shippingId, + "shippingCom": shippingCom, + "shipmentNum": shipmentNum, + "userDefined": userDefined, + "moreShipmentNum": moreShipmentNum, + } + // 发送POST请求 + request := gorequest.New() + resp, body, errs := request.Post(kUrl). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "*/*"). + Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"). + Type("form"). // 关键:明确指定为表单格式 + Timeout(30 * time.Second). + Send(formData). + End() + // 错误处理 + if len(errs) > 0 { + return "", fmt.Errorf("登录请求失败: %v", errs) + } + // 检查HTTP状态码 + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + // 异常处理 + if response["ErrorResponse"] != nil { + var kwAPIResponse KwAPIResponse + if err := json.Unmarshal([]byte(body), &kwAPIResponse); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", kwAPIResponse.ErrorResponse.Msg, kwAPIResponse.ErrorResponse.Code) + } + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// 孔网订单同步 +func kongfzOrderSynchronization(appId int, appSecret, accessToken string, shippingComName string, + orderId int, shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum string) (string, error) { + // 获取配送方式列表 + deliveryMethodList, err := kongfzDeliveryMethodList(appId, appSecret, accessToken) + if err != nil { + return "", err + } + // 解析配送方式 + var kw KwAPIResponse + if err := json.Unmarshal([]byte(deliveryMethodList), &kw); err != nil { + return "", fmt.Errorf("解析JSON失败: %v", err) + } + // 根据快递公司名称查找对应的配送方式和快递公司代号 + for _, shippingMethod := range kw.SuccessResponse { + for _, companies := range shippingMethod.Companies { + if shippingComName == companies.ShippingComName { + shippingId = shippingMethod.ShippingId + shippingCom = companies.ShippingCom + } + } + } + // 如果未找到,使用默认值 + if shippingId == "" { + shippingId = "express" + shippingCom = "other" + } + // 执行订单发货 + orderDeliver, err := kongfzOrderDeliver(appId, appSecret, accessToken, orderId, shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum) + if err != nil { + return "", err + } + return orderDeliver, err +} + // =============== 辅助函数 ============== + +// 获取GMT+8当前时间的字符串格式 +func getCurrentTimeGMT8() string { + // 创建北京时间(GMT+8) + loc, _ := time.LoadLocation("Asia/Shanghai") + now := time.Now().In(loc) + return now.Format("2006-01-02 15:04:05") +} + // 替换所有空白字符为空格 func cleanString(s string) string { s = strings.ReplaceAll(s, "\n", "") @@ -1584,7 +2027,7 @@ func removeDuplicates(s string) string { return result.String() } -// 验证日期格式 +// 验证日期格式并转换为时间戳 func validateDateFormat(dateStr string) int64 { // 去除前后空格 dateStr = strings.TrimSpace(dateStr) @@ -1645,7 +2088,7 @@ func validateDateFormat(dateStr string) int64 { return parsedTime.Unix() } -// 初始化 +// 初始化配置 func initializeConfig(config Config) { // 设置全局配置 cf = config @@ -1653,18 +2096,20 @@ func initializeConfig(config Config) { // =================== C 导出函数 ======================= -// 孔网登录 +// OutLogin 孔网登录 // //export OutLogin func OutLogin(username, password *C.char) *C.char { goUsername := C.GoString(username) goPassword := C.GoString(password) respToken, err := outLogin(goUsername, goPassword) + // 构建响应数据 resp := struct { Token string `json:"token"` }{ Token: respToken, } + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1691,12 +2136,13 @@ func OutLogin(username, password *C.char) *C.char { return C.CString(string(jsonData)) } -// 获取孔网用户信息 +// OutGetUserMsg 获取孔网用户信息 // //export OutGetUserMsg func OutGetUserMsg(token *C.char) *C.char { goToken := C.GoString(token) userInfo, err := outGetUserMsg(goToken) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1723,7 +2169,7 @@ func OutGetUserMsg(token *C.char) *C.char { return C.CString(string(jsonData)) } -// 获取商品模版--已登的店铺 +// OutGetGoodsTplMsg 获取商品模版--已登的店铺 // //export OutGetGoodsTplMsg func OutGetGoodsTplMsg(token, proxy, itemId *C.char) *C.char { @@ -1731,6 +2177,7 @@ func OutGetGoodsTplMsg(token, proxy, itemId *C.char) *C.char { goProxy := C.GoString(proxy) goItemId := C.GoString(itemId) info, err := outGetGoodsTplMsg(goToken, goProxy, goItemId) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1757,7 +2204,7 @@ func OutGetGoodsTplMsg(token, proxy, itemId *C.char) *C.char { return C.CString(string(jsonData)) } -// 获取商品列表-已登的店铺 +// OutGetGoodsListMsgFromSelfShop 获取商品列表-已登的店铺 // //export OutGetGoodsListMsgFromSelfShop func OutGetGoodsListMsgFromSelfShop(token, proxy, itemSn, priceMin, priceMax *C.char, startCreateTime *C.char, @@ -1774,6 +2221,7 @@ func OutGetGoodsListMsgFromSelfShop(token, proxy, itemSn, priceMin, priceMax *C. goPage := int(page) goSize := int(size) info, err := outGetGoodsListMsgFromSelfShop(goToken, goProxy, goItemSn, goPriceMin, goPriceMax, goStartCreateTime, goEndCreateTime, goRequestType, goIsItemSnEqual, goPage, goSize) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1800,7 +2248,7 @@ func OutGetGoodsListMsgFromSelfShop(token, proxy, itemSn, priceMin, priceMax *C. return C.CString(string(jsonData)) } -// 新增商品-已登的店铺(带有Out的都非官方标准接口) +// OutAddGoods 新增商品-已登的店铺 // //export OutAddGoods func OutAddGoods(token, proxy, formData *C.char) *C.char { @@ -1808,6 +2256,7 @@ func OutAddGoods(token, proxy, formData *C.char) *C.char { goProxy := C.GoString(proxy) goFormData := C.GoString(formData) info, err := outAddGoods(goToken, goProxy, goFormData) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1834,7 +2283,7 @@ func OutAddGoods(token, proxy, formData *C.char) *C.char { return C.CString(string(jsonData)) } -// 删除商品-已登的店铺 +// OutDelGoodsFromSelfShop 删除商品-已登的店铺 // //export OutDelGoodsFromSelfShop func OutDelGoodsFromSelfShop(token, proxy, itemId *C.char) *C.char { @@ -1842,6 +2291,7 @@ func OutDelGoodsFromSelfShop(token, proxy, itemId *C.char) *C.char { goProxy := C.GoString(proxy) goItemId := C.GoString(itemId) info, err := outDelGoodsFromSelfShop(goToken, goProxy, goItemId) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1868,7 +2318,7 @@ func OutDelGoodsFromSelfShop(token, proxy, itemId *C.char) *C.char { return C.CString(string(jsonData)) } -// 获取孔网商品图片和信息(官图和拍图)-带有店铺过滤 +// OutGetImageFilterShopId 获取孔网商品图片和信息(官图和拍图)-带有店铺过滤 // //export OutGetImageFilterShopId func OutGetImageFilterShopId(token, proxy, isbn *C.char, shopId C.int, isLiveImage C.int, isReturnMsg C.int) *C.char { @@ -1879,6 +2329,7 @@ func OutGetImageFilterShopId(token, proxy, isbn *C.char, shopId C.int, isLiveIma goIsLiveImage := int(isLiveImage) goIsReturnMsg := int(isReturnMsg) info, err := outGetImageFilterShopId(goToken, goProxy, goIsbn, goShopId, goIsLiveImage, goIsReturnMsg) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1905,16 +2356,17 @@ func OutGetImageFilterShopId(token, proxy, isbn *C.char, shopId C.int, isLiveIma return C.CString(string(jsonData)) } -// 获取孔网商品图片和信息(官图和拍图) +// OutGetImageByIsbn 获取孔网商品图片和信息(官图和拍图) // //export OutGetImageByIsbn -func OutGetImageByIsbn(token, isbn, proxy *C.char, isLiveImage C.int, isReturnMsg C.int) *C.char { +func OutGetImageByIsbn(token, proxy, isbn *C.char, isLiveImage C.int, isReturnMsg C.int) *C.char { goToken := C.GoString(token) - goIsbn := C.GoString(isbn) goProxy := C.GoString(proxy) + goIsbn := C.GoString(isbn) goIsLiveImage := int(isLiveImage) goIsReturnMsg := int(isReturnMsg) - bookInfo, err := outGetImageByIsbn(goToken, goIsbn, goProxy, goIsLiveImage, goIsReturnMsg) + bookInfo, err := outGetImageByIsbn(goToken, goProxy, goIsbn, goIsLiveImage, goIsReturnMsg) + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1941,7 +2393,7 @@ func OutGetImageByIsbn(token, isbn, proxy *C.char, isLiveImage C.int, isReturnMs return C.CString(string(jsonData)) } -// 获取商品列表通过店铺ID +// OutGetGoodsListMsgByShopId 获取商品列表通过店铺ID // //export OutGetGoodsListMsgByShopId func OutGetGoodsListMsgByShopId(shopId C.int, proxy *C.char, retPrice C.int, isImage C.int, sortType *C.char, sort *C.char, priceMin C.float, priceMax C.float, pageNum, returnNum C.int) *C.char { @@ -1967,6 +2419,7 @@ func OutGetGoodsListMsgByShopId(shopId C.int, proxy *C.char, retPrice C.int, isI PNum: pNum, BookInfo: books, } + // 构建API响应 var apiResponse APIResponse if err != nil { apiResponse = APIResponse{ @@ -1993,7 +2446,7 @@ func OutGetGoodsListMsgByShopId(shopId C.int, proxy *C.char, retPrice C.int, isI return C.CString(string(jsonData)) } -// 获取商品信息通过商品详情链接 +// OutGetGoodsMsgByDetailUrl 获取商品信息通过商品详情链接 // //export OutGetGoodsMsgByDetailUrl func OutGetGoodsMsgByDetailUrl(detailUrl, proxy *C.char) *C.char { @@ -2027,7 +2480,7 @@ func OutGetGoodsMsgByDetailUrl(detailUrl, proxy *C.char) *C.char { return C.CString(string(jsonData)) } -// 获取销量榜商品列表 +// OutGetTopGoodsListMsg 获取销量榜商品列表 // //export OutGetTopGoodsListMsg func OutGetTopGoodsListMsg(catId C.int, proxy *C.char) *C.char { @@ -2061,7 +2514,66 @@ func OutGetTopGoodsListMsg(catId C.int, proxy *C.char) *C.char { return C.CString(string(jsonData)) } -// 初始化配置 +// KongfzDeliveryMethodList 获取配送方式列表 +// +//export KongfzDeliveryMethodList +func KongfzDeliveryMethodList(appId C.int, appSecret, accessToken *C.char) *C.char { + goAppId := int(appId) + goAppSecret := C.GoString(appSecret) + goAccessToken := C.GoString(accessToken) + list, err := kongfzDeliveryMethodList(goAppId, goAppSecret, goAccessToken) + if err != nil { + return C.CString(fmt.Sprint(err)) + } + return C.CString(list) +} + +// KongfzOrderDeliver 订单发货 +// +//export KongfzOrderDeliver +func KongfzOrderDeliver(appId C.int, appSecret, accessToken *C.char, + orderId C.int, shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum *C.char) *C.char { + goAppId := int(appId) + goAppSecret := C.GoString(appSecret) + goAccessToken := C.GoString(accessToken) + goOrderId := int(orderId) + goShippingId := C.GoString(shippingId) + goShippingCom := C.GoString(shippingCom) + goShipmentNum := C.GoString(shipmentNum) + goUserDefined := C.GoString(userDefined) + goMoreShipmentNum := C.GoString(moreShipmentNum) + deliver, err := kongfzOrderDeliver(goAppId, goAppSecret, goAccessToken, + goOrderId, goShippingId, goShippingCom, goShipmentNum, goUserDefined, goMoreShipmentNum) + if err != nil { + return C.CString(fmt.Sprint(err)) + } + return C.CString(deliver) +} + +// KongfzOrderSynchronization 孔网订单同步 +// +//export KongfzOrderSynchronization +func KongfzOrderSynchronization(appId C.int, appSecret, accessToken, shippingComName *C.char, + orderId C.int, shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum *C.char) *C.char { + goAppId := int(appId) + goAppSecret := C.GoString(appSecret) + goAccessToken := C.GoString(accessToken) + goShippingComName := C.GoString(shippingComName) + goOrderId := int(orderId) + goShippingId := C.GoString(shippingId) + goShippingCom := C.GoString(shippingCom) + goShipmentNum := C.GoString(shipmentNum) + goUserDefined := C.GoString(userDefined) + goMoreShipmentNum := C.GoString(moreShipmentNum) + synchronization, err := kongfzOrderSynchronization(goAppId, goAppSecret, goAccessToken, goShippingComName, + goOrderId, goShippingId, goShippingCom, goShipmentNum, goUserDefined, goMoreShipmentNum) + if err != nil { + return C.CString(fmt.Sprint(err)) + } + return C.CString(synchronization) +} + +// Initialize 初始化配置 // //export Initialize func Initialize(configJSON *C.char) *C.char { @@ -2077,7 +2589,7 @@ func Initialize(configJSON *C.char) *C.char { return C.CString(`{"success":true,"message":"初始化成功"}`) } -// 释放C字符串内存 +// FreeCString 释放C字符串内存 // //export FreeCString func FreeCString(str *C.char) { diff --git a/md/es.md b/md/es.md new file mode 100644 index 0000000..8702889 --- /dev/null +++ b/md/es.md @@ -0,0 +1,346 @@ +# es.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件~~~~ +```gotemplate +// EsDLL Elasticsearch工具DLL结构 +type esDLL struct { + dll *syscall.DLL + listAllIndices *syscall.Proc // 查询所有索引 + getIndicesInfo *syscall.Proc // 获取所有索引的详细信息 + getIndexDetail *syscall.Proc // 获取单个索引的详细信息 + createIndex *syscall.Proc // 创建索引 + deleteIndex *syscall.Proc // 删除索引 + getDocumentCount *syscall.Proc // 获取索引文档数量 + createDocument *syscall.Proc // 创建文档 + getDocument *syscall.Proc // 根据ID获取文档 + updateDocument *syscall.Proc // 更新文档 + deleteDocument *syscall.Proc // 删除文档 + searchDocuments *syscall.Proc // 搜索文档 + freeCString *syscall.Proc // 释放C字符串 +} + +// 初始化esDLL +func InitEsDLL() (*esDLL, error) { + dllPath := filepath.Join("dll", "es.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("es DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载es DLL 失败: %s", err) + } else { + return &esDLL{ + dll: dll, + listAllIndices: dll.MustFindProc("ListAllIndices"), + getIndicesInfo: dll.MustFindProc("GetIndicesInfo"), + getIndexDetail: dll.MustFindProc("GetIndexDetail"), + createIndex: dll.MustFindProc("CreateIndex"), + deleteIndex: dll.MustFindProc("DeleteIndex"), + getDocumentCount: dll.MustFindProc("GetDocumentCount"), + createDocument: dll.MustFindProc("CreateDocument"), + getDocument: dll.MustFindProc("GetDocument"), + updateDocument: dll.MustFindProc("UpdateDocument"), + deleteDocument: dll.MustFindProc("DeleteDocument"), + searchDocuments: dll.MustFindProc("SearchDocuments"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitEsDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *esDLL) cStr(p uintptr) string { + if p == 0 { + return "" + } + b := []byte{} + for i := uintptr(0); ; i++ { + c := *(*byte)(unsafe.Pointer(p + i)) + if c == 0 { + break + } + b = append(b, c) + } + s := string(b) + if m.freeCString != nil { + m.freeCString.Call(p) + } + return s +} +``` + +## 2. 使用dll函数示例 +```gotemplate +// 查询所有索引 +func (m *esDLL) ListAllIndices() (string, error) { + proc, err := m.dll.FindProc("ListAllIndices") + if err != nil { + return "", fmt.Errorf("找不到函数 ListAllIndices: %v", err) + } + resultPtr, _, _ := proc.Call() + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1.查询所有索引--ListAllIndices +### 请求信息 +```gotemplate +dll.ListAllIndices() +``` +### 请求参数 +无 +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": ["index1", "index2", "index3"] +} +``` + +## 2. 获取所有索引的详细信息--GetIndicesInfo +### 请求信息 +```gotemplate +dll.GetIndicesInfo() +``` +### 请求参数 +无 +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": [ + { + "index": "index1", + "health": "green", + "status": "open", + "uuid": "abc123", + "pri": "1", + "rep": "1", + "docs.count": "100", + "docs.deleted": "0", + "store.size": "1.2kb", + "pri.store.size": "600b" + } + ] +} +``` + +## 3. 获取单个索引的详细信息--GetIndexDetail +### 请求信息 +```gotemplate +dll.GetIndexDetail(indexName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| indexName | string | 是 | 索引名称 | +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": { + "index1": { + "aliases": {}, + "mappings": {}, + "settings": { + "index": { + "creation_date": "1234567890000", + "number_of_shards": "1", + "number_of_replicas": "1", + "uuid": "abc123", + "version": { + "created": "7080099" + } + } + } + } + } +} +``` + +## 4. 创建索引--CreateIndex +### 请求信息 +```gotemplate +dll.CreateIndex(indexName, mapping) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +| mapping | string | 是 | 索引映射的JSON字符串 | +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": { + + } +} +``` + +## 5. 删除索引--DeleteIndex +### 请求信息 +```gotemplate +dll.DeleteIndex(indexName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +### 响应示例 +```json +{ + "success": true, + "message": "" +} +``` + +## 6. 获取索引文档数量--GetDocumentCount +### 请求信息 +```gotemplate +dll.GetDocumentCount(indexName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": 100 +} +``` + +## 7. 创建文档--CreateDocument +### 请求信息 +```gotemplate +dll.CreateDocument(indexName, id, doc) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +| id | string | 是 | 文档ID | +| doc | string | 是 | 文档内容的JSON字符串 | +### 响应示例 +```json +{ + "success": true, + "message": "" +} +``` + +## 8. 根据ID获取文档--GetDocument +### 请求信息 +```gotemplate +dll.GetDocument(indexName, id) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +| id | string | 是 | 文档ID | +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": { + "_id": "1", + "_index": "index1", + "_source": { + "title": "示例文档", + "content": "这是文档内容" + } + } +} +``` + +## 9. 更新文档--UpdateDocument +### 请求信息 +```gotemplate +dll.UpdateDocument(indexName, id, updateData) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +| id | string | 是 | 文档ID | +| updateData | string | 是 | 更新数据的JSON字符串 | +### 响应示例 +```json +{ + "success": true, + "message": "" +} +``` + +## 10. 删除文档--DeleteDocument +### 请求信息 +```gotemplate +dll.DeleteDocument(indexName, id) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|---------------| +| indexName | string | 是 | 索引名称 | +| id | string | 是 | 文档ID | +### 响应示例 +```json +{ + "success": true, + "message": "" +} +``` + +## 11. 搜索文档--SearchDocuments +### 请求信息 +```gotemplate +dll.SearchDocuments(indexName, query) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|--------------| +| indexName | string | 是 | 索引名称 | +| query | string | 是 | 查询条件的JSON字符串| +#### query示例 +```gotemplate +query := map[string]interface{}{ + "query": map[string]interface{}{ + "term": map[string]interface{}{ + fieldName: fieldValue, + }, + }, + "size": size, +} +``` +### 响应示例 +```json +{ + "success": true, + "message": "", + "data": [ + { + "_id": "1", + "title": "示例文档", + "content": "这是文档内容" + }, + { + "_id": "2", + "title": "另一个文档", + "content": "更多内容" + } + ] +} +``` \ No newline at end of file diff --git a/md/kongfz.md b/md/kongfz.md index c53c06d..4ce0ae9 100644 --- a/md/kongfz.md +++ b/md/kongfz.md @@ -1567,17 +1567,165 @@ dll.OutGetTopGoodsListMsg(catId,proxy) } ``` +## 12.获取配送方式列表--KongfzDeliveryMethodList +### 请求信息 +```gotemplate +dll.KongfzDeliveryMethodList(appId C.int, appSecret, accessToken *C.char) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--------|----|----------------------------| +| appId | int | 是 | 开放平台分配给应用的AppId | +| appSecret | string | 是 | App密钥 | +| accessToken | string | 否 | 用户登录授权成功后,开放平台颁发给应用的授权信息。 | +### 响应示例 +```json +{ + "requestId": "EGLJzOwutBCR9RDE", + "requestMethod": "kongfz.delivery.method.list", + "successResponse": [ + { + "shippingId": "registerPost", + "shippingName": "挂号印刷品", + "isDefault": false, + "companies": [ + { + "shippingCom": "registeredPrint", + "shippingComName": "邮局", + "isDefault": false + } + ] + }, + { + "shippingId": "express", + "shippingName": "快递", + "isDefault": false, + "companies": [ + { + "shippingCom": "huitongkuaidi", + "shippingComName": "百世快递", + "isDefault": true + }, + { + "shippingCom": "yunda", + "shippingComName": "韵达快递", + "isDefault": false, + }, + ] + }, + { + "shippingId": "generalParcel", + "shippingName": "普通包裹", + "isDefault": false, + "companies": [ + { + "shippingCom": "generalParcel", + "shippingComName": "邮局", + "isDefault": false + } + ] + }, + { + "shippingId": "ems", + "shippingName": "EMS", + "isDefault": false, + "companies": [ + { + "shippingCom": "ems", + "shippingComName": "邮局", + "isDefault": false + } + ] + }, + { + "shippingId": "noLogistics", + "shippingName": "无需物流", + "isDefault": false, + "companies": [] + } + ], + "errorResponse": null +} +``` + +## 13.订单发货--KongfzOrderDeliver +### 请求信息 +```gotemplate +dll.KongfzOrderDeliver(appId, appSecret, accessToken , +orderId , shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum ) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--------|----|-------------| +| appId | int | 是 | 开放平台分配给应用的AppId | +| appSecret | string | 是 | App密钥 | +| accessToken | string | 否 | 用户登录授权成功后,开放平台颁发给应用的授权信息 | +| orderId | int | 是 | 订单编号 | +| shippingId | string | 是 | 配送方式 | +| shippingCom | string | 否 | 快递公司。当shippingId!=noLogistics时,此参数为必填。取值参考kongfz.delivery.method.list接口的返回值 | +| shipmentNum | string | 否 | 快递单号。当shippingId!=noLogistics时,此参数为必填。 | +| userDefined | string | 否 | 用户自定义物流公司。当shippingCom=other时,此参数为必填。 | +| moreShipmentNum | string | 否 | 填写更多的快递单号,以逗号分隔。 | +### 响应示例 +```json +{ + "requestId": "bbXrGb2dOBRRDRBL", + "requestMethod": "kongfz.order.deliver", + "successResponse": { + "order": { + "orderId": 73609014, + "remark": "订单发货成功", + "updateTime": "2019-05-22 16:32:08" + } + }, + "errorResponse": null +} +``` + +## 14.孔网订单同步--KongfzOrderSynchronization +### 请求信息 +```gotemplate +result, err := dll.KongfzOrderSynchronization(appId, appSecret, accessToken, shippingComName, +orderId, shippingId, shippingCom, shipmentNum, userDefined, moreShipmentNum) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|----|-----------------------------------------------------------------------------| +| appId | int | 是 | 开放平台分配给应用的AppId | +| appSecret | string | 是 | App密钥 | +| accessToken | string | 否 | 用户登录授权成功后,开放平台颁发给应用的授权信息 | +| shippingComName | string | 是 | 快递名称 | +| orderId | int | 是 | 订单编号 | +| shippingId | string | 是 | 配送方式 | +| shippingCom | string | 否 | 快递公司。当shippingId!=noLogistics时,此参数为必填。取值参考kongfz.delivery.method.list接口的返回值 | +| shipmentNum | string | 否 | 快递单号。当shippingId!=noLogistics时,此参数为必填。 | +| userDefined | string | 否 | 用户自定义物流公司。当shippingCom=other时,此参数为必填。 | +| moreShipmentNum | string | 否 | 填写更多的快递单号,以逗号分隔。 | +### 响应示例 +```json +{ + "requestId": "bbXrGb2dOBRRDRBL", + "requestMethod": "kongfz.order.deliver", + "successResponse": { + "order": { + "orderId": 73609014, + "remark": "订单发货成功", + "updateTime": "2019-05-22 16:32:08" + } + }, + "errorResponse": null +} +``` + ## 12.初始化--Initialize(可以不调用) ### 请求信息 ```gotemplate result, err := dll.Initialize(configJSON) ``` ### 请求参数 - | 参数名 | 类型 | 必填 | 说明 | |--|--|--|----------------| | configJSON | string | 是 | 配置信息的 JSON 字符串 | - ### 响应示例 ```json { diff --git a/md/pdd.md b/md/pdd.md new file mode 100644 index 0000000..82c1e4a --- /dev/null +++ b/md/pdd.md @@ -0,0 +1,495 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 +```gotemplate +// PddDLL 拼多多工具DLL结构 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + freeCString *syscall.Proc // 释放C字符串 +} + +// 初始化pddDLL +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +dll, err := InitPddDLL() +``` + +### 获取C字符串 +```gotemplate +// cStr 获取C字符串 +func (m *pddDLL) 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 *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + + resultPtr, _, _ := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + + result := m.cStr(resultPtr) + return result, nil +} +``` + +# 接口详情 +## 1. 类目预测--PddGoodsOuterCatMappingGet +### 请求信息 +```gotemplate +dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, +outerCatId, outerCatName, outerGoodsName) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| outerCatId | string | 是 | 外部平台类目ID | +| outerCatName | string | 是 | 外部平台类目名称 | +| outerGoodsName | string | 是 | 外部商品名称 | +### 响应示例 +```json +{ + "outer_cat_mapping_get_response": { + "cat_id2": 16028, + "cat_id3": 16031, + "cat_id1": 15543, + "request_id": "17666480184871649", + "cat_id4": 0 + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 2. 快递公司查看--PddLogisticsCompaniesGet +### 请求信息 +```gotemplate +dll.PddLogisticsCompaniesGet(clientId, clientSecret) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +### 响应示例 +```json +{ + "logistics_companies_get_response": { + "logistics_companies": [ + { + "available": 1, + "code": "SF", + "id": 1, + "logistics_company": "顺丰速运" + }, + { + "available": 1, + "code": "STO", + "id": 2, + "logistics_company": "申通快递" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 3. erp打单信息同步--PddErpOrderSync +### 请求信息 +```gotemplate +dll.PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871650" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 4. 拼多多订单同步--PddOrderSynchronization +### 请求信息 +```gotemplate +dll.PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsId, +orderSn, orderState, waybillNo) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| logisticsCompany | string | 是 | 物流公司名称 | +| logisticsId | string | 是 | 物流公司ID | +| orderSn | string | 是 | 拼多多订单号 | +| orderState | string | 是 | 订单状态 | +| waybillNo | string | 是 | 运单号 | +### 响应示例 +```json +{ + "erp_order_sync_response": { + "is_success": true, + "request_id": "17666480184871651" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 5. 商品图片上传接口--PddGoodsImgUpload +### 请求信息 +```gotemplate +dll.PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +### 响应示例 +```json +{ + "goods_img_upload_response": { + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "request_id": "17666480184871652" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 6. 商品新增接口--PddGoodsAdd +### 请求信息 +```gotemplate +dll.PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| goodsAddJson | string | 是 | 商品信息JSON字符串 | +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456789, + "goods_name": "测试商品", + "goods_sn": "G202501200001", + "request_id": "17666480184871653" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 7. 联合拼多多图片上传的商品新增--SelfPddGoodsAdd +### 请求信息 +```gotemplate +dll.SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| filePath | string | 是 | 图片文件路径 | +| goodsAddJson | string | 是 | 商品信息JSON字符串(不需包含image_url)| +#### 接口说明 +此接口为组合接口,内部执行以下步骤: +1.上传商品主图文件到拼多多服务器 +2.获取图片URL并自动填充到商品信息中 +3.调用商品新增接口创建商品 +#### 商品信息JSON结构示例 +```json +{ + "goods_name": "测试商品", + "goods_desc": "商品描述", + "cat_id": 20111, + "goods_type": 1, + "market_price": 9900, + "is_folt": false, + "is_pre_sale": false, + "is_refundable": true, + "shipment_limit_second": 86400, + "cost_template_id": 10001, + "image_url": "", + "carousel_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "detail_gallery": [ + "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg" + ], + "sku_list": [ + { + "out_sku_sn": "SKU001", + "price": 8900, + "quantity": 100, + "spec_id_list": "1001:10001", + "sku_properties": [ + { + "ref_pid": 1001, + "value": "红色", + "vid": 10001, + "punit": "个" + } + ], + "is_onsale": 1, + "limit_quantity": 10, + "multi_price": 8500, + "thumb_url": "http://oms-imageimg.pinduoduo.com/upload/2025/01/20/e9a8c1b6e1a84f1d8d7c3a8b9e2f5c7d.jpg", + "weight": 500 + } + ] +} +``` +### 响应示例 +```json +{ + "goods_add_response": { + "goods_id": 123456790, + "goods_name": "测试商品", + "goods_sn": "G202501200002", + "request_id": "17666480184871654" + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` + +## 8. 批量数据解密脱敏接口--PddOpenDecryptMaskBatch +### 请求信息 +```gotemplate +dll.PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|--|--|----------| +| clientId | string | 是 | 拼多多开放平台ClientID | +| clientSecret | string | 是 | 拼多多开放平台ClientSecret | +| accessToken | string | 是 | 授权令牌 | +| reqJson | string | 是 | 信息JSON字符串 | +#### 信息JSON结构示例 +```json +[ + { + "data_tag": "251229-272441044622514", + "encrypted_data": "~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~" + } +] +``` +### 响应示例 +```json +{ + "open_decrypt_mask_batch_response": { + "data_decrypt_list": [ + { + "data_tag": "str", + "data_type": 0, + "decrypted_data": "str", + "encrypted_data": "str", + "error_code": 0, + "error_msg": "str" + } + ] + } +} +``` +### 错误响应示例 +```json +{ + "error_response": { + "error_msg": "公共参数错误:type", + "sub_msg": "", + "sub_code": null, + "error_code": 10001, + "request_id": "15440104776643887" + } +} +``` \ No newline at end of file diff --git a/md/xy.md b/md/xy.md new file mode 100644 index 0000000..1936f97 --- /dev/null +++ b/md/xy.md @@ -0,0 +1,655 @@ +# pdd.dll 使用教程 +## 1.创建DLL工具实例 +### 加载DLL文件 + + + +# 接口详情 +## 1. 查询快递公司--ExecuteOpenExpressCompanies +### 请求信息 +```gotemplate +dll.ExecuteOpenExpressCompanies(appid, timestamp, sign) +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|---------|----|----------| +| appid | string | 是 | 开放平台的AppKey | +| timestamp | integer | 否 | 当前时间戳(单位秒,5分钟内有效) | +| sign | string | 是 | 签名MD5值(参考签名说明) | +### 响应示例 +```json +{ + "code": 0, + "msg": "OK", + "data": { + "list": [ + { + "code": "shentong", + "express_alias": "申通", + "express_name": "申通快递", + "is_hot": true + }, + { + "code": "yunda", + "express_alias": "韵达", + "express_name": "韵达快递", + "is_hot": true + }, + { + "code": "htky", + "express_alias": "极兔", + "express_name": "极兔-原百世快递", + "is_hot": true + }, + { + "code": "youzhengguonei", + "express_alias": "", + "express_name": "邮政快递包裹", + "is_hot": true + }, + { + "code": "yuantong", + "express_alias": "圆通", + "express_name": "圆通速递", + "is_hot": true + }, + { + "code": "zhongtong", + "express_alias": "中通", + "express_name": "中通快递", + "is_hot": true + }, + { + "code": "zhaijisong", + "express_alias": "", + "express_name": "宅急送", + "is_hot": true + }, + { + "code": "tiantian", + "express_alias": "天天", + "express_name": "天天快递", + "is_hot": true + }, + { + "code": "shunfeng", + "express_alias": "顺丰", + "express_name": "顺丰速运", + "is_hot": true + }, + { + "code": "ems", + "express_alias": "", + "express_name": "EMS", + "is_hot": true + }, + { + "code": "other", + "express_alias": "", + "express_name": "其他", + "is_hot": true + }, + { + "code": "baishikuaidi", + "express_alias": "", + "express_name": "百世快递", + "is_hot": false + }, + { + "code": "sxjdfreight", + "express_alias": "", + "express_name": "顺心捷达", + "is_hot": false + }, + { + "code": "wanjiawuliu", + "express_alias": "", + "express_name": "万家物流", + "is_hot": false + }, + { + "code": "taijin", + "express_alias": "", + "express_name": "泰进物流", + "is_hot": false + }, + { + "code": "tcat", + "express_alias": "", + "express_name": "黑猫宅急便", + "is_hot": false + }, + { + "code": "tiandihuayu", + "express_alias": "", + "express_name": "天地华宇", + "is_hot": false + }, + { + "code": "usps", + "express_alias": "USPS", + "express_name": "美国邮政", + "is_hot": false + }, + { + "code": "yafengsudi", + "express_alias": "", + "express_name": "亚风速递", + "is_hot": false + }, + { + "code": "sut56", + "express_alias": "", + "express_name": "速通物流", + "is_hot": false + }, + { + "code": "suning", + "express_alias": "苏宁", + "express_name": "苏宁快递", + "is_hot": false + }, + { + "code": "suer", + "express_alias": "速尔", + "express_name": "速尔快运", + "is_hot": false + }, + { + "code": "shenweizhaipei", + "express_alias": "", + "express_name": "神威宅配", + "is_hot": false + }, + { + "code": "shenghuiwuliu", + "express_alias": "", + "express_name": "盛辉物流", + "is_hot": false + }, + { + "code": "shangqiao56", + "express_alias": "", + "express_name": "商桥物流", + "is_hot": false + }, + { + "code": "rufengda", + "express_alias": "如风达", + "express_name": "如风达配送", + "is_hot": false + }, + { + "code": "rrs", + "express_alias": "日日顺", + "express_name": "日日顺物流", + "is_hot": false + }, + { + "code": "youzhengbk", + "express_alias": "", + "express_name": "邮政标准快递", + "is_hot": false + }, + { + "code": "ztky", + "express_alias": "", + "express_name": "中铁快运", + "is_hot": false + }, + { + "code": "zhongtongkuaiyun", + "express_alias": "", + "express_name": "中通快运", + "is_hot": false + }, + { + "code": "zhongtiewuliu", + "express_alias": "中铁飞豹", + "express_name": "中铁物流", + "is_hot": false + }, + { + "code": "zengyisudi", + "express_alias": "", + "express_name": "增益速递", + "is_hot": false + }, + { + "code": "yundatongcheng", + "express_alias": "", + "express_name": "韵达同城", + "is_hot": false + }, + { + "code": "yundakuaiyun", + "express_alias": "", + "express_name": "韵达快运", + "is_hot": false + }, + { + "code": "yujiawl", + "express_alias": "", + "express_name": "山东宇佳物流", + "is_hot": false + }, + { + "code": "yuantongcainiancang", + "express_alias": "", + "express_name": "圆通菜鸟仓", + "is_hot": false + }, + { + "code": "yuanshuochengnuoda", + "express_alias": "", + "express_name": "圆硕承诺达特快", + "is_hot": false + }, + { + "code": "wanxiangwuliu", + "express_alias": "万象物流", + "express_name": "A1万象物流", + "is_hot": false + }, + { + "code": "youxinwuliu", + "express_alias": "", + "express_name": "优信物流", + "is_hot": false + }, + { + "code": "youshuwuliu", + "express_alias": "优速", + "express_name": "优速快递", + "is_hot": false + }, + { + "code": "yimidida", + "express_alias": "壹米滴答", + "express_name": "壹米滴答快运", + "is_hot": false + }, + { + "code": "ycgky", + "express_alias": "", + "express_name": "远成快运", + "is_hot": false + }, + { + "code": "post", + "express_alias": "", + "express_name": "中国邮政", + "is_hot": false + }, + { + "code": "xinzebangwuliu", + "express_alias": "", + "express_name": "鑫泽邦物流", + "is_hot": false + }, + { + "code": "xinfengwuliu", + "express_alias": "", + "express_name": "信丰物流", + "is_hot": false + }, + { + "code": "xinbangwuliu", + "express_alias": "", + "express_name": "新邦物流", + "is_hot": false + }, + { + "code": "debangwuliu", + "express_alias": "", + "express_name": "德邦物流", + "is_hot": false + }, + { + "code": "huayuwuliu", + "express_alias": "", + "express_name": "重庆华宇物流", + "is_hot": false + }, + { + "code": "haoyaoshizijian", + "express_alias": "", + "express_name": "好药师自建物流", + "is_hot": false + }, + { + "code": "guotongkuaidi", + "express_alias": "国通", + "express_name": "国通快递", + "is_hot": false + }, + { + "code": "ganzhongnengda", + "express_alias": "", + "express_name": "能达速递", + "is_hot": false + }, + { + "code": "fushisudi", + "express_alias": "", + "express_name": "服饰速递", + "is_hot": false + }, + { + "code": "fengwang", + "express_alias": "丰网", + "express_name": "丰网速运", + "is_hot": false + }, + { + "code": "exfresh", + "express_alias": "安鲜达", + "express_name": "安鲜达快递", + "is_hot": false + }, + { + "code": "esb", + "express_alias": "", + "express_name": "E速宝", + "is_hot": false + }, + { + "code": "dsukuaidi", + "express_alias": "D速快递", + "express_name": "D速物流", + "is_hot": false + }, + { + "code": "diandiansong", + "express_alias": "", + "express_name": "点点送", + "is_hot": false + }, + { + "code": "jd", + "express_alias": "京东", + "express_name": "京东物流", + "is_hot": false + }, + { + "code": "debangkuaidi", + "express_alias": "", + "express_name": "德邦快递", + "is_hot": false + }, + { + "code": "cszx", + "express_alias": "", + "express_name": "城市之星", + "is_hot": false + }, + { + "code": "canpostfr", + "express_alias": "", + "express_name": "加拿大邮政", + "is_hot": false + }, + { + "code": "cainiaodj-woaijia", + "express_alias": "", + "express_name": "菜鸟大件-沃埃家", + "is_hot": false + }, + { + "code": "baishiyp", + "express_alias": "", + "express_name": "百世云配", + "is_hot": false + }, + { + "code": "baishikuaiyun", + "express_alias": "", + "express_name": "百世快运", + "is_hot": false + }, + { + "code": "astexpress", + "express_alias": "安世通", + "express_name": "安世通国际快递", + "is_hot": false + }, + { + "code": "anxl", + "express_alias": "", + "express_name": "安迅物流", + "is_hot": false + }, + { + "code": "annto", + "express_alias": "", + "express_name": "安得物流", + "is_hot": false + }, + { + "code": "jiuyescm", + "express_alias": "", + "express_name": "九曳鲜配", + "is_hot": false + }, + { + "code": "quanfengkuaidi", + "express_alias": "", + "express_name": "全峰快递", + "is_hot": false + }, + { + "code": "annengwuliu", + "express_alias": "", + "express_name": "安能物流", + "is_hot": false + }, + { + "code": "pingandatengfei", + "express_alias": "平安达腾飞", + "express_name": "平安达腾飞快递", + "is_hot": false + }, + { + "code": "menduimen", + "express_alias": "", + "express_name": "门对门", + "is_hot": false + }, + { + "code": "linshiwuliu", + "express_alias": "", + "express_name": "林氏物流", + "is_hot": false + }, + { + "code": "lianhaowuliu", + "express_alias": "", + "express_name": "联昊通", + "is_hot": false + }, + { + "code": "lianbangkuaidi", + "express_alias": "", + "express_name": "联邦快递", + "is_hot": false + }, + { + "code": "kuayue", + "express_alias": "跨越", + "express_name": "跨越速运", + "is_hot": false + }, + { + "code": "kahangtianxia", + "express_alias": "", + "express_name": "卡行天下", + "is_hot": false + }, + { + "code": "jtexpress", + "express_alias": "极兔", + "express_name": "极兔速递", + "is_hot": false + }, + { + "code": "quanyikuaidi", + "express_alias": "", + "express_name": "全一快递", + "is_hot": false + }, + { + "code": "jinguangsudikuaijian", + "express_alias": "", + "express_name": "京广速递", + "is_hot": false + }, + { + "code": "jindouyunjiaoche", + "express_alias": "", + "express_name": "筋斗云轿车物流", + "is_hot": false + }, + { + "code": "jiazhuang-zhengjia", + "express_alias": "", + "express_name": "家装-正佳", + "is_hot": false + }, + { + "code": "jiazhuang-zhelian", + "express_alias": "", + "express_name": "家装-浙联", + "is_hot": false + }, + { + "code": "jiazhuang-sfc", + "express_alias": "", + "express_name": "家装-顺风车", + "is_hot": false + }, + { + "code": "jiayunmeiwuliu", + "express_alias": "加运美", + "express_name": "加运美速递", + "is_hot": false + }, + { + "code": "jiayiwuliu", + "express_alias": "", + "express_name": "佳怡物流", + "is_hot": false + }, + { + "code": "jiajiwuliu", + "express_alias": "", + "express_name": "佳吉快运", + "is_hot": false + }, + { + "code": "jgwl", + "express_alias": "", + "express_name": "景光物流", + "is_hot": false + } + ] + } +} +``` + +## 2. 订单物流发货--ExecuteOpenOrderShip +### 请求信息 +```gotemplate +dll.ExecuteOpenOrderShip(json) +``` +### json样式 +```json +{ + "order_no": "1339920336328048683", + "ship_name": "张三", + "ship_mobile": "13800138000", + "ship_district_id": 440305, + "ship_prov_name": "广东省", + "ship_city_name": "深圳市", + "ship_area_name": "南山区", + "ship_address": "侨香路西丽街道丰泽园仓储中心", + "waybill_no": "25051016899982", + "express_name": "其他", + "express_code": "qita" +} +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|---------|----|-------------------| +| appid | string | 是 | 开放平台的AppKey | +| timestamp | integer | 否 | 当前时间戳(单位秒,5分钟内有效) | +| seller_id | integer | 否 | 商家ID | +| sign | string | 是 | 签名MD5值(参考签名说明) | +| order_no | string | 是 | 闲鱼订单号 | +| ship_name | string | 否 | 寄件方姓名 | +| ship_mobile | string | 否 | 寄件方号码 | +| ship_district_id | int | 否 | 寄件方所在地区ID | +| ship_prov_name | string | 否 | 寄件方所在省份 | +| ship_city_name | string | 否 | 寄件方所在城市 | +| ship_area_name | string | 否 | 寄件方所在地区 | +| ship_address | string | 否 | 寄件方详细地址 | +| waybill_no | string | 否 | 快递单号 | +| express_code | string | 否 | 快递公司代码 | +| express_name | string | 否 | 快递公司名称 | + +### 响应示例 +```json +{ + "code": 0, + "msg": "ok", + "data": {} +} +``` + +## 3. 闲鱼订单同步--ExecuteXyOrderSynchronization +### 请求信息 +```gotemplate +dll.ExecuteXyOrderSynchronization(json) +``` +### json样式 +```json +{ + "order_no": "1339920336328048683", + "ship_name": "张三", + "ship_mobile": "13800138000", + "ship_district_id": 440305, + "ship_prov_name": "广东省", + "ship_city_name": "深圳市", + "ship_area_name": "南山区", + "ship_address": "侨香路西丽街道丰泽园仓储中心", + "waybill_no": "25051016899982", + "express_name": "其他", + "express_code": "qita" +} +``` +### 请求参数 +| 参数名 | 类型 | 必填 | 说明 | +|--|---------|----|-------------------| +| appid | string | 是 | 开放平台的AppKey | +| timestamp | integer | 否 | 当前时间戳(单位秒,5分钟内有效) | +| seller_id | integer | 否 | 商家ID | +| sign | string | 是 | 签名MD5值(参考签名说明) | +| order_no | string | 是 | 闲鱼订单号 | +| ship_name | string | 否 | 寄件方姓名 | +| ship_mobile | string | 否 | 寄件方号码 | +| ship_district_id | int | 否 | 寄件方所在地区ID | +| ship_prov_name | string | 否 | 寄件方所在省份 | +| ship_city_name | string | 否 | 寄件方所在城市 | +| ship_area_name | string | 否 | 寄件方所在地区 | +| ship_address | string | 否 | 寄件方详细地址 | +| waybill_no | string | 否 | 快递单号 | +| express_code | string | 否 | 快递公司代码 | +| express_name | string | 否 | 快递公司名称 | + +### 响应示例 +```json +{ + "code": 0, + "msg": "ok", + "data": {} +} +``` \ No newline at end of file diff --git a/pdd/dll/pdd.dll b/pdd/dll/pdd.dll new file mode 100644 index 0000000..62d652e Binary files /dev/null and b/pdd/dll/pdd.dll differ diff --git a/pdd/dll/pdd.h b/pdd/dll/pdd.h new file mode 100644 index 0000000..1a876b6 --- /dev/null +++ b/pdd/dll/pdd.h @@ -0,0 +1,125 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package command-line-arguments */ + + +#line 1 "cgo-builtin-export-prolog" + +#include + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { const char *p; ptrdiff_t n; } _GoString_; +extern size_t _GoStringLen(_GoString_ s); +extern const char *_GoStringPtr(_GoString_ s); +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + + +#line 3 "pdd.go" + +#include + +#line 1 "cgo-generated-wrapper" + + +/* End of preamble from import "C" comments. */ + + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; +typedef unsigned char GoUint8; +typedef short GoInt16; +typedef unsigned short GoUint16; +typedef int GoInt32; +typedef unsigned int GoUint32; +typedef long long GoInt64; +typedef unsigned long long GoUint64; +typedef GoInt64 GoInt; +typedef GoUint64 GoUint; +typedef size_t GoUintptr; +typedef float GoFloat32; +typedef double GoFloat64; +#ifdef _MSC_VER +#if !defined(__cplusplus) || _MSVC_LANG <= 201402L +#include +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +#include +typedef std::complex GoComplex64; +typedef std::complex GoComplex128; +#endif +#else +typedef float _Complex GoComplex64; +typedef double _Complex GoComplex128; +#endif + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; +#endif +typedef void *GoMap; +typedef void *GoChan; +typedef struct { void *t; void *v; } GoInterface; +typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + + +// PddGoodsOuterCatMappingGet 类目预测 +// +extern __declspec(dllexport) char* PddGoodsOuterCatMappingGet(char* clientId, char* clientSecret, char* accessToken, char* outerCatId, char* outerCatName, char* outerGoodsName); + +// PddLogisticsCompaniesGet 快递公司查看 +// +extern __declspec(dllexport) char* PddLogisticsCompaniesGet(char* clientId, char* clientSecret); + +// PddErpOrderSync erp打单信息同步 +// +extern __declspec(dllexport) char* PddErpOrderSync(char* clientId, char* clientSecret, char* accessToken, char* logisticsId, char* orderSn, char* orderState, char* waybillNo); + +// PddOrderSynchronization 拼多多订单同步 +// +extern __declspec(dllexport) char* PddOrderSynchronization(char* clientId, char* clientSecret, char* accessToken, char* logisticsCompany, char* logisticsId, char* orderSn, char* orderState, char* waybillNo); + +// PddGoodsImgUpload 商品图片上传接口 +// +extern __declspec(dllexport) char* PddGoodsImgUpload(char* clientId, char* clientSecret, char* accessToken, char* filePath); + +// PddGoodsAdd 商品新增接口 +// +extern __declspec(dllexport) char* PddGoodsAdd(char* clientId, char* clientSecret, char* accessToken, char* goodsAddJson); + +// SelfPddGoodsAdd 联合拼多多图片上传的商品新增 +// +extern __declspec(dllexport) char* SelfPddGoodsAdd(char* clientId, char* clientSecret, char* accessToken, char* filePath, char* goodsAddJson); + +// 释放C字符串内存 +// +extern __declspec(dllexport) void FreeCString(char* str); + +#ifdef __cplusplus +} +#endif diff --git a/pdd/pdd.go b/pdd/pdd.go new file mode 100644 index 0000000..71a210f --- /dev/null +++ b/pdd/pdd.go @@ -0,0 +1,886 @@ +package main + +/* +#include +*/ +import "C" +import ( + "bytes" + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/parnurzeal/gorequest" + "io" + "mime/multipart" + "os" + "path/filepath" + "sort" + "strings" + "time" + "unsafe" +) + +// ErrorResponse 错误响应结构 +type ErrorResponse struct { + ErrorMsg string `json:"error_msg"` // 错误信息 + SubMsg string `json:"sub_msg"` // 子错误信息 + SubCode interface{} `json:"sub_code"` // 使用json.RawMessage处理null和不同类型 + ErrorCode int `json:"error_code"` // 错误代码 + RequestID string `json:"request_id"` // 请求ID +} + +// ErrorWrapper 最外层错误响应包装 +type ErrorWrapper struct { + ErrorResponse ErrorResponse `json:"error_response"` // 错误响应 +} + +// generateSign 生成签名 +func generateSign(params map[string]interface{}, clientSecret string) string { + // 获取所有键并排序 + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + // 按键名升序排序 + sort.Strings(keys) + + // 拼接签名字符串 + var signStr strings.Builder + for _, k := range keys { + // 跳过 sign 参数(签名本身不参与签名) + if strings.ToLower(k) == "sign" { + continue + } + + value := "" + if params[k] != nil { + value = fmt.Sprintf("%v", params[k]) // 将任意值转为字符串 + } + // 按照文档格式:参数名+参数值 + signStr.WriteString(k + value) + } + + signString := signStr.String() + + // 拼接client_secret: client_secret + 参数串 + client_secret + data := clientSecret + signString + clientSecret + + // MD5加密并转大写 + hasher := md5.New() + hasher.Write([]byte(data)) + return strings.ToUpper(hex.EncodeToString(hasher.Sum(nil))) +} + +// 类目预测 +func pddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + // API地址 + url := fmt.Sprint("https://gw-api.pinduoduo.com/api/router") + // 当前时间戳 + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + + // 生成签名参数 + params := map[string]interface{}{ + "type": "pdd.goods.outer.cat.mapping.get", // API类型 + "data_type": "JSON", // 数据类型 + "client_id": clientId, // 客户端ID + "access_token": accessToken, // 访问令牌 + "outer_cat_id": outerCatId, // 外部类目ID + "outer_cat_name": outerCatName, // 外部类目名称 + "outer_goods_name": outerGoodsName, // 外部商品名称 + "timestamp": timestamp, // 时间戳 + } + // 生成签名 + sign := generateSign(params, clientSecret) + // 请求体 + formData := map[string]interface{}{ + "type": "pdd.goods.outer.cat.mapping.get", + "data_type": "JSON", + "client_id": clientId, + "access_token": accessToken, + "outer_cat_id": outerCatId, + "outer_cat_name": outerCatName, + "outer_goods_name": outerGoodsName, + "timestamp": timestamp, + "sign": sign, // 签名 + } + request := gorequest.New() + resp, body, errs := request.Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(30 * time.Second). // 30秒超时 + Send(formData). // 发送表单数据 + End() // 结束请求 + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// 快递公司查看 +func pddLogisticsCompaniesGet(clientId, clientSecret string) (string, error) { + url := fmt.Sprint("https://gw-api.pinduoduo.com/api/router") + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + // 生成签名参数 + params := map[string]interface{}{ + "type": "pdd.logistics.companies.get", // API类型:获取物流公司 + "data_type": "JSON", + "client_id": clientId, + "timestamp": timestamp, + } + sign := generateSign(params, clientSecret) + // 请求体 + formData := map[string]interface{}{ + "type": "pdd.logistics.companies.get", + "data_type": "JSON", + "client_id": clientId, + "timestamp": timestamp, + "sign": sign, + } + request := gorequest.New() + resp, body, errs := request.Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(30 * time.Second). + Send(formData). + End() + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + + // 异常处理:检查是否有错误响应 + if response["error_response"] != nil { + var errorWrapper ErrorWrapper + // 解析响应 + if err := json.Unmarshal([]byte(body), &errorWrapper); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", errorWrapper.ErrorResponse.ErrorMsg, errorWrapper.ErrorResponse.ErrorCode) + } + + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + + return string(responseJSON), nil +} + +// erp打单信息同步 +func pddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, + orderSn, orderState, waybillNo string) (string, error) { + url := fmt.Sprint("https://gw-api.pinduoduo.com/api/router") + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + // 生成签名 + params := map[string]interface{}{ + "type": "pdd.erp.order.sync", // API类型:ERP订单同步 + "data_type": "JSON", + "client_id": clientId, + "access_token": accessToken, + "logistics_id": logisticsId, // 物流公司ID + "order_sn": orderSn, // 订单号 + "order_state": orderState, // 订单状态 + "waybill_no": waybillNo, // 运单号 + "timestamp": timestamp, + } + sign := generateSign(params, clientSecret) + // 请求体 + formData := map[string]interface{}{ + "type": "pdd.erp.order.sync", + "data_type": "JSON", + "client_id": clientId, + "access_token": accessToken, + "logistics_id": logisticsId, + "order_sn": orderSn, + "order_state": orderState, + "waybill_no": waybillNo, + "timestamp": timestamp, + "sign": sign, + } + request := gorequest.New() + resp, body, errs := request.Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(30 * time.Second). + Send(formData). + End() + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + + // 异常处理 + if response["error_response"] != nil { + var errorWrapper ErrorWrapper + // 解析响应 + if err := json.Unmarshal([]byte(body), &errorWrapper); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", errorWrapper.ErrorResponse.ErrorMsg, errorWrapper.ErrorResponse.ErrorCode) + } + + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// LogisticsCompany 物流公司信息结构体 +type LogisticsCompany struct { + Available int `json:"available"` // 是否可用 + Code string `json:"code"` // 物流公司代码 + ID int `json:"id"` // 物流公司ID + LogisticsCompany string `json:"logistics_company"` // 物流公司名称 +} + +// LogisticsCompaniesGetResponse 物流公司列表响应结构体 +type LogisticsCompaniesGetResponse struct { + LogisticsCompanies []LogisticsCompany `json:"logistics_companies"` // 物流公司列表 +} + +// LogisticsResponse 最外层响应结构体 +type LogisticsResponse struct { + LogisticsCompaniesGetResponse LogisticsCompaniesGetResponse `json:"logistics_companies_get_response"` // 物流公司响应 +} + +// 拼多多订单同步(组合接口:先查物流公司,再同步订单) +func pddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsId, + orderSn, orderState, waybillNo string) (string, error) { + // 1. 获取物流公司列表 + logisticsCompaniesGet, err := pddLogisticsCompaniesGet(clientId, clientSecret) + if err != nil { + return "", err + } + // 2. 解析物流公司响应 + var response LogisticsResponse + if err := json.Unmarshal([]byte(logisticsCompaniesGet), &response); err != nil { + return "", fmt.Errorf("解析JSON失败: %v", err) + } + // 3. 根据物流公司名称查找对应的物流公司ID + for _, lc := range response.LogisticsCompaniesGetResponse.LogisticsCompanies { + if lc.LogisticsCompany == logisticsCompany { + logisticsId = fmt.Sprintf("%d", lc.ID) + break + } + } + // 4. 执行ERP订单同步 + erpOrderSync, err := pddErpOrderSync(clientId, clientSecret, accessToken, + logisticsId, orderSn, orderState, waybillNo) + if err != nil { + return "", err + } + return erpOrderSync, nil +} + +// 商品图片上传接口 +func pddGoodsImgUpload(clientId, clientSecret, accessToken string, filePath string) (string, error) { + url := fmt.Sprint("https://gw-upload.pinduoduo.com/api/upload") // 上传专用地址 + + // 1. 打开文件 + file, err := os.Open(filePath) + if err != nil { + return "", fmt.Errorf("打开文件失败: %v", err) + } + defer file.Close() // 确保文件关闭 + + // 当前时间戳 + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + // 生成签名参数 + params := map[string]interface{}{ + "type": "pdd.goods.img.upload", // API类型:商品图片上传 + "data_type": "JSON", + "client_id": clientId, + "access_token": accessToken, + "timestamp": timestamp, + } + + sign := generateSign(params, clientSecret) + + // 创建multipart/form-data请求体 + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 读取文件内容 + fileData, err := os.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("读取文件失败: %v", err) + } + + // 添加文件字段 + part, err := writer.CreateFormFile("file", filepath.Base(filePath)) + if err != nil { + return "", fmt.Errorf("创建表单文件失败: %v", err) + } + _, err = io.Copy(part, bytes.NewReader(fileData)) + if err != nil { + return "", fmt.Errorf("复制文件内容失败: %v", err) + } + + // 添加其他参数到multipart表单 + for key, value := range params { + writer.WriteField(key, fmt.Sprintf("%v", value)) + } + writer.WriteField("sign", sign) // 添加签名 + writer.Close() // 关闭writer,完成表单构建 + + // 发送POST请求 + request := gorequest.New() + resp, respBody, errs := request.Post(url). + Timeout(30*time.Second). + Set("Content-Type", writer.FormDataContentType()). // 设置Content-Type + Send(body.String()). // 发送请求体 + End() + + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(respBody), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + + // 异常处理 + if response["error_response"] != nil { + var errorWrapper ErrorWrapper + // 解析响应 + if err := json.Unmarshal([]byte(respBody), &errorWrapper); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", errorWrapper.ErrorResponse.ErrorMsg, errorWrapper.ErrorResponse.ErrorCode) + } + + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// GoodsAddRequest 商品添加/编辑请求结构体 +type GoodsAddRequest struct { + AutoFillSpuProperty bool `json:"auto_fill_spu_property,omitempty"` // 是否自动补充标品属性 + BadFruitClaim int `json:"bad_fruit_claim,omitempty"` // 坏果包赔 + BuyLimit int64 `json:"buy_limit,omitempty"` // 限购次数 + CarouselGallery []string `json:"carousel_gallery"` // 商品轮播图 + CarouselVideo []CarouselVideo `json:"carousel_video,omitempty"` // 商品视频 + CarouselVideoURL string `json:"carousel_video_url,omitempty"` // 轮播视频 + CatID int64 `json:"cat_id"` // 叶子类目ID + CostTemplateID int64 `json:"cost_template_id"` // 物流运费模板ID + CountryID int `json:"country_id"` // 地区/国家ID + CustomerNum int64 `json:"customer_num,omitempty"` // 团购人数 + Customs string `json:"customs,omitempty"` // 海关名称 + DeliveryOneDay int `json:"delivery_one_day,omitempty"` // 是否当日发货 + DeliveryType int `json:"delivery_type,omitempty"` // 发货方式 + DetailGallery []string `json:"detail_gallery"` // 商品详情图 + ElecGoodsAttributes ElecGoodsAttributes `json:"elec_goods_attributes,omitempty"` // 卡券类商品属性 + GoodsDesc string `json:"goods_desc,omitempty"` // 商品描述 + GoodsName string `json:"goods_name"` // 商品标题 + GoodsProperties []GoodsProperty `json:"goods_properties,omitempty"` // 商品属性列表 + GoodsTradeAttr GoodsTradeAttr `json:"goods_trade_attr,omitempty"` // 日历商品交易相关信息 + GoodsTravelAttr GoodsTravelAttr `json:"goods_travel_attr,omitempty"` // 商品出行信息 + GoodsType int `json:"goods_type"` // 商品类型 + IgnoreEditWarn bool `json:"ignore_edit_warn,omitempty"` // 是否获取商品发布警告信息 + ImageURL string `json:"image_url,omitempty"` // 商品主图 + InvoiceStatus bool `json:"invoice_status,omitempty"` // 是否支持开票 + IsCustoms bool `json:"is_customs,omitempty"` // 是否需要上报海关 + IsFolt bool `json:"is_folt"` // 是否支持假一赔十 + IsGroupPreSale int `json:"is_group_pre_sale,omitempty"` // 是否成团预售 + IsPreSale bool `json:"is_pre_sale"` // 是否预售 + IsRefundable bool `json:"is_refundable"` // 是否7天无理由退换货 + IsSkuPreSale int `json:"is_sku_pre_sale,omitempty"` // 是否sku预售 + LackOfWeightClaim int `json:"lack_of_weight_claim,omitempty"` // 缺重包退 + LocalServiceIDList []int `json:"local_service_id_list,omitempty"` // 本地服务id + MaiJiaZiTi string `json:"mai_jia_zi_ti,omitempty"` // 买家自提模版id + MarketPrice int64 `json:"market_price"` // 参考价格(分) + OrderLimit int `json:"order_limit,omitempty"` // 单次限量 + OriginCountryID int `json:"origin_country_id,omitempty"` // 原产地id + OutGoodsID string `json:"out_goods_id,omitempty"` // 商品外部编码 + OutSourceGoodsID string `json:"out_source_goods_id,omitempty"` // 第三方商品Id + OutSourceType int `json:"out_source_type,omitempty"` // 第三方商品来源 + OverseaGoods OverseaGoods `json:"oversea_goods,omitempty"` // 海淘商品信息 + OverseaType int `json:"oversea_type,omitempty"` // oversea_type + PreSaleTime int64 `json:"pre_sale_time,omitempty"` // 预售时间 + PrivacyDelivery int `json:"privacy_delivery,omitempty"` // 保密发货 + QuanGuoLianBao int `json:"quan_guo_lian_bao,omitempty"` // 是否支持全国联保 + SecondHand bool `json:"second_hand"` // 是否二手商品 + ShangMenAnZhuang string `json:"shang_men_an_zhuang,omitempty"` // 上门安装模版id + ShipmentLimitSecond int64 `json:"shipment_limit_second"` // 承诺发货时间(秒) + ShopGroupID int64 `json:"shop_group_id,omitempty"` // 门店组id + SizeSpecID int64 `json:"size_spec_id,omitempty"` // 尺码表id + SkuList []Sku `json:"sku_list"` // sku对象列表 + SkuType int `json:"sku_type,omitempty"` // 库存方式 + SongHuoAnZhuang string `json:"song_huo_an_zhuang,omitempty"` // 送货入户并安装模版id + SongHuoRuHu string `json:"song_huo_ru_hu,omitempty"` // 送货入户模版id + TinyName string `json:"tiny_name,omitempty"` // 短标题 + TwoPiecesDiscount int `json:"two_pieces_discount,omitempty"` // 满2件折扣 + Warehouse string `json:"warehouse,omitempty"` // 保税仓 + WarmTips string `json:"warm_tips,omitempty"` // 水果类目温馨提示 + ZhiHuanBuXiu int `json:"zhi_huan_bu_xiu,omitempty"` // 只换不修的天数 +} + +// CarouselVideo 轮播视频 +type CarouselVideo struct { + FileID int64 `json:"file_id,omitempty"` // 商品视频id + VideoURL string `json:"video_url,omitempty"` // 商品视频url +} + +// ElecGoodsAttributes 卡券类商品属性 +type ElecGoodsAttributes struct { + BeginTime int64 `json:"begin_time,omitempty"` // 开始时间 + DaysTime int `json:"days_time,omitempty"` // 天数内有效 + EndTime int64 `json:"end_time,omitempty"` // 截止时间 + TimeType int `json:"time_type,omitempty"` // 卡券核销类型 +} + +// GoodsProperty 商品属性 +type GoodsProperty struct { + GroupID int `json:"group_id,omitempty"` // 属性值分组ID + ImgURL string `json:"img_url,omitempty"` // 图片url + Note string `json:"note,omitempty"` // 备注 + ParentSpecID int64 `json:"parent_spec_id,omitempty"` // 父规格ID + RefPid int64 `json:"ref_pid,omitempty"` // 引用属性id + SpecID int64 `json:"spec_id,omitempty"` // 规格ID + TemplatePid int64 `json:"template_pid,omitempty"` // 模板属性id + Value string `json:"value,omitempty"` // 属性值 + ValueUnit string `json:"value_unit,omitempty"` // 属性单位 + Vid int64 `json:"vid,omitempty"` // 属性值id + //TableInfo []TableValueList `json:"table_info,omitempty"` // 成分表表单信息 +} + +//// TableValueList 表单内容列表 +//type TableValueList struct { +// ColumnType int `json:"column_type,omitempty"` // 列类型 1-材质成分百分比 +// Unit string `json:"unit,omitempty"` // 表单单位,材质成分表时为:% +// Value string `json:"value,omitempty"` // 表单值,材质成分表时为:占比百分值 +//} + +// GoodsTradeAttr 日历商品交易相关信息 +type GoodsTradeAttr struct { + AdvancesDays int `json:"advances_days,omitempty"` // 提前预定天数 + BookingNotes BookingNotes `json:"booking_notes,omitempty"` // 预订须知 + LifeSpan int `json:"life_span,omitempty"` // 卡券有效期 +} + +// BookingNotes 预订须知 +type BookingNotes struct { + URL *string `json:"url,omitempty"` // 预定须知图片地址 +} + +// GoodsTravelAttr 商品出行信息 +type GoodsTravelAttr struct { + NeedTourist bool `json:"need_tourist,omitempty"` // 出行人是否必填 + Type int `json:"type,omitempty"` // 日历商品类型 +} + +// OverseaGoods 海淘商品信息 +type OverseaGoods struct { + BondedWarehouseKey string `json:"bonded_warehouse_key"` // 保税仓唯一标识 + ConsumptionTaxRate int `json:"consumption_tax_rate,omitempty"` // 消费税率 + CustomsBroker string `json:"customs_broker,omitempty"` // 清关服务商 + HsCode string `json:"hs_code,omitempty"` // 海关编号 + ValueAddedTaxRate int `json:"value_added_tax_rate,omitempty"` // 增值税率 +} + +// Sku SKU信息 +type Sku struct { + IsOnsale int `json:"is_onsale"` // sku上架状态 + Length int64 `json:"length,omitempty"` // sku送装参数:长度 + LimitQuantity int64 `json:"limit_quantity"` // sku购买限制 + MultiPrice int64 `json:"multi_price"` // 商品团购价格 + OutSkuSn string `json:"out_sku_sn,omitempty"` // 商品sku外部编码 + OutSourceSkuID string `json:"out_source_sku_id,omitempty"` // 第三方sku Id + OverseaSku OverseaSku `json:"oversea_sku,omitempty"` // 海淘sku信息 + Price int64 `json:"price"` // 商品单买价格 + Quantity int64 `json:"quantity"` // 商品sku库存数量 + SkuPreSaleTime int `json:"sku_pre_sale_time,omitempty"` // sku预售时间戳 + SkuProperties []SkuProperty `json:"sku_properties"` // sku属性 + SpecIDList string `json:"spec_id_list"` // 商品规格列表 + ThumbURL string `json:"thumb_url"` // sku缩略图 + Weight int64 `json:"weight"` // 重量(g) +} + +// OverseaSku 海淘SKU信息 +type OverseaSku struct { + MeasurementCode string `json:"measurement_code"` // 计量单位编码 + Specifications string `json:"specifications"` // 规格 + Taxation int `json:"taxation"` // 税费 +} + +// SkuProperty SKU属性 +type SkuProperty struct { + Punit string `json:"punit"` // 属性单位 + RefPid int64 `json:"ref_pid"` // 属性id + Value string `json:"value"` // 属性值 + Vid int64 `json:"vid"` // 属性值id +} + +// 商品新增接口 +func pddGoodsAdd(clientId, clientSecret, accessToken string, goodsAddJson string) (string, error) { + url := fmt.Sprint("http://gw-api.pinduoduo.com/api/router") + + // 当前时间戳 + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + // 生成签名参数 + params := map[string]interface{}{ + "type": "pdd.goods.add", // API类型:商品新增接口 + "data_type": "JSON", + "client_id": clientId, + "access_token": accessToken, + "timestamp": timestamp, + } + + if goodsAddJson == "" { + return "", fmt.Errorf("goodsAddJson 参数为空!") + } + // 将JSON参数合并到params中 + toParams, err := addStructToParams(goodsAddJson, params) + if err != nil { + return "", err + } + + sign := generateSign(toParams, clientSecret) + + if sign == "" { + return "", fmt.Errorf("生成 sign 签名错误!") + } + params["sign"] = sign + + request := gorequest.New() + resp, body, errs := request.Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(30 * time.Second). + Send(params). + End() + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + + // 异常处理 + if response["error_response"] != nil { + var errorWrapper ErrorWrapper + // 解析响应 + if err := json.Unmarshal([]byte(body), &errorWrapper); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", errorWrapper.ErrorResponse.ErrorMsg, errorWrapper.ErrorResponse.ErrorCode) + } + + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// 联合拼多多图片上传的商品新增(组合接口) +func selfPddGoodsAdd(clientId, clientSecret, accessToken string, filePath string, goodsAddJson string) (string, error) { + // 1. 上传商品图片 + upload, err := pddGoodsImgUpload(clientId, clientSecret, accessToken, filePath) + if err != nil { + return "", err + } + // 2. 解析商品添加请求JSON + var goodsAddRequest GoodsAddRequest + if err := json.Unmarshal([]byte(goodsAddJson), &goodsAddRequest); err != nil { + return "", fmt.Errorf("解析 goodsAddJson 失败: %v", err) + } + // 3. 设置图片URL + goodsAddRequest.ImageURL = upload + // 4. 重新序列化为JSON + goodsAddRequestStr, err := json.Marshal(goodsAddRequest) + if err != nil { + return "", fmt.Errorf("序列化 goodsAddRequest 失败:%v", err) + } + // 5. 调用商品添加接口 + add, err := pddGoodsAdd(clientId, clientSecret, accessToken, string(goodsAddRequestStr)) + if err != nil { + return "", err + } + return add, nil +} + +// 批量数据解密脱敏接口 +func pddOpenDecryptMaskBatch(clientId, clientSecret, accessToken string, reqJson string) (string, error) { + url := fmt.Sprint("http://gw-api.pinduoduo.com/api/router") + // 当前时间戳 + timestamp := fmt.Sprintf("%d", time.Now().Unix()) + // 生成签名参数 + params := map[string]interface{}{ + "type": "pdd.open.decrypt.mask.batch", // API类型:批量解密脱敏 + "data_type": "JSON", + "client_id": clientId, + "access_token": accessToken, + "timestamp": timestamp, + } + params["data_list"] = reqJson // 添加数据列表 + + sign := generateSign(params, clientSecret) + + if sign == "" { + return "", fmt.Errorf("生成 sign 签名错误!") + } + params["sign"] = sign + request := gorequest.New() + resp, body, errs := request.Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(30 * time.Second). + Send(params). + End() + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + + if resp.StatusCode != 200 { + return "", fmt.Errorf("HTTP状态码异常: %d, 响应: %s", resp.StatusCode, body) + } + + // 解析响应 + var response map[string]interface{} + if err := json.Unmarshal([]byte(body), &response); err != nil { + return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, body) + } + + // 异常处理 + if response["error_response"] != nil { + var errorWrapper ErrorWrapper + // 解析响应 + if err := json.Unmarshal([]byte(body), &errorWrapper); err != nil { + return "", fmt.Errorf("解析 errorWrapper 失败: %v, 响应内容: %s", err, body) + } + return "", fmt.Errorf("请求失败: %v, 错误码: %d", errorWrapper.ErrorResponse.ErrorMsg, errorWrapper.ErrorResponse.ErrorCode) + } + + // 转换成json字符串 + responseJSON, err := json.Marshal(response) + if err != nil { + return "", fmt.Errorf("JSON序列化失败: %v", err) + } + return string(responseJSON), nil +} + +// =========================== 辅助函数 ============================ +// 将结构体的字段添加到 params 映射中 +func addStructToParams(req string, params map[string]interface{}) (map[string]interface{}, error) { + // 将JSON字符串解析为map + var tempMap map[string]interface{} + if err := json.Unmarshal([]byte(req), &tempMap); err != nil { + return nil, fmt.Errorf("解析 req json失败:%v ", err) + } + // 合并到params + for k, v := range tempMap { + // 只添加非nil的值 + if v != nil { + params[k] = v + } + } + return params, nil +} + +// ========================== C 导入函数 =================== + +// PddGoodsOuterCatMappingGet 类目预测 +// +//export PddGoodsOuterCatMappingGet +func PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName *C.char) *C.char { + // 将C字符串转换为Go字符串 + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goOuterCatId := C.GoString(outerCatId) + goOuterCatName := C.GoString(outerCatName) + goOuterGoodsName := C.GoString(outerGoodsName) + // 调用Go函数 + info, err := pddGoodsOuterCatMappingGet(goClientId, goClientSecret, goAccessToken, goOuterCatId, goOuterCatName, goOuterGoodsName) + if err != nil { + return C.CString(err.Error()) // 返回错误信息 + } + return C.CString(info) // 返回成功信息 +} + +// PddLogisticsCompaniesGet 快递公司查看 +// +//export PddLogisticsCompaniesGet +func PddLogisticsCompaniesGet(clientId, clientSecret *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + info, err := pddLogisticsCompaniesGet(goClientId, goClientSecret) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// PddErpOrderSync erp打单信息同步 +// +//export PddErpOrderSync +func PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, + orderSn, orderState, waybillNo *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goLogisticsId := C.GoString(logisticsId) + goOrderSn := C.GoString(orderSn) + goOrderState := C.GoString(orderState) + goWaybillNo := C.GoString(waybillNo) + info, err := pddErpOrderSync(goClientId, goClientSecret, goAccessToken, goLogisticsId, goOrderSn, goOrderState, goWaybillNo) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// PddOrderSynchronization 拼多多订单同步 +// +//export PddOrderSynchronization +func PddOrderSynchronization(clientId, clientSecret, accessToken, logisticsCompany, logisticsId, + orderSn, orderState, waybillNo *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goLogisticsCompany := C.GoString(logisticsCompany) + goLogisticsId := C.GoString(logisticsId) + goOrderSn := C.GoString(orderSn) + goOrderState := C.GoString(orderState) + goWaybillNo := C.GoString(waybillNo) + info, err := pddOrderSynchronization(goClientId, goClientSecret, goAccessToken, goLogisticsCompany, goLogisticsId, goOrderSn, goOrderState, goWaybillNo) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// PddGoodsImgUpload 商品图片上传接口 +// +//export PddGoodsImgUpload +func PddGoodsImgUpload(clientId, clientSecret, accessToken, filePath *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goFilePath := C.GoString(filePath) + info, err := pddGoodsImgUpload(goClientId, goClientSecret, goAccessToken, goFilePath) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// PddGoodsAdd 商品新增接口 +// +//export PddGoodsAdd +func PddGoodsAdd(clientId, clientSecret, accessToken, goodsAddJson *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goGoodsAddJson := C.GoString(goodsAddJson) + info, err := pddGoodsAdd(goClientId, goClientSecret, goAccessToken, goGoodsAddJson) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// SelfPddGoodsAdd 联合拼多多图片上传的商品新增 +// +//export SelfPddGoodsAdd +func SelfPddGoodsAdd(clientId, clientSecret, accessToken, filePath, goodsAddJson *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goFilePath := C.GoString(filePath) + goGoodsAddJson := C.GoString(goodsAddJson) + info, err := selfPddGoodsAdd(goClientId, goClientSecret, goAccessToken, goFilePath, goGoodsAddJson) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// PddOpenDecryptMaskBatch 批量数据解密脱敏接口 +// +//export PddOpenDecryptMaskBatch +func PddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, reqJson *C.char) *C.char { + goClientId := C.GoString(clientId) + goClientSecret := C.GoString(clientSecret) + goAccessToken := C.GoString(accessToken) + goReqJson := C.GoString(reqJson) + info, err := pddOpenDecryptMaskBatch(goClientId, goClientSecret, goAccessToken, goReqJson) + if err != nil { + return C.CString(err.Error()) + } + return C.CString(info) +} + +// FreeCString 释放C字符串内存 +// +//export FreeCString +func FreeCString(str *C.char) { + C.free(unsafe.Pointer(str)) +} + +// main函数 +func main() { +} diff --git a/pdd/pddDll.go b/pdd/pddDll.go new file mode 100644 index 0000000..e2adadd --- /dev/null +++ b/pdd/pddDll.go @@ -0,0 +1,232 @@ +package main + +import "C" +import ( + "fmt" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// pddDLL 拼多多API调用结构体 +type pddDLL struct { + dll *syscall.DLL + pddGoodsOuterCatMappingGet *syscall.Proc // 类目预测 + pddLogisticsCompaniesGet *syscall.Proc // 快递公司查看 + pddErpOrderSync *syscall.Proc // erp打单信息同步 + freeCString *syscall.Proc // 释放C字符串 +} + +// InitPddDLL 初始化pddDLL +func InitPddDLL() (*pddDLL, error) { + dllPath := filepath.Join("pdd", "dll", "pdd.dll") + if _, err := os.Stat(dllPath); os.IsNotExist(err) { + return nil, fmt.Errorf("pdd DLL 不存在: %s", dllPath) + } + if dll, err := syscall.LoadDLL(dllPath); err != nil { + return nil, fmt.Errorf("加载pdd DLL 失败: %s", err) + } else { + return &pddDLL{ + dll: dll, + pddGoodsOuterCatMappingGet: dll.MustFindProc("PddGoodsOuterCatMappingGet"), + pddLogisticsCompaniesGet: dll.MustFindProc("PddLogisticsCompaniesGet"), + pddErpOrderSync: dll.MustFindProc("PddErpOrderSync"), + freeCString: dll.MustFindProc("FreeCString"), + }, nil + } +} + +// cStr 获取C字符串 +func (m *pddDLL) 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 +} + +// PddGoodsOuterCatMappingGet 类目预测 +func (m *pddDLL) PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, + outerCatId, outerCatName, outerGoodsName string) (string, error) { + proc, err := m.dll.FindProc("PddGoodsOuterCatMappingGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddGoodsOuterCatMappingGet 函数: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + outerCatIdPtr, _ := syscall.BytePtrFromString(outerCatId) + outerCatNamePtr, _ := syscall.BytePtrFromString(outerCatName) + outerGoodsNamePtr, _ := syscall.BytePtrFromString(outerGoodsName) + info, _, err := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(outerCatIdPtr)), + uintptr(unsafe.Pointer(outerCatNamePtr)), + uintptr(unsafe.Pointer(outerGoodsNamePtr)), + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 PddGoodsOuterCatMappingGet 失败: %v", err) + } + return m.cStr(info), nil +} + +// PddLogisticsCompaniesGet 快递公司查看 +func (m *pddDLL) PddLogisticsCompaniesGet(clientId, clientSecret string) (string, error) { + proc, err := m.dll.FindProc("PddLogisticsCompaniesGet") + if err != nil { + return "", fmt.Errorf("找不到函数 PddLogisticsCompaniesGet 函数: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + info, _, err := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 PddGoodsOuterCatMappingGet 失败: %v", err) + } + return m.cStr(info), nil +} + +// PddErpOrderSync erp打单信息同步 +func (m *pddDLL) PddErpOrderSync(clientId, clientSecret, accessToken, logisticsId, + orderSn, orderState, waybillNo string) (string, error) { + proc, err := m.dll.FindProc("PddErpOrderSync") + if err != nil { + return "", fmt.Errorf("找不到函数 PddErpOrderSync 函数: %v", err) + } + + clientIdPtr, _ := syscall.BytePtrFromString(clientId) + clientSecretPtr, _ := syscall.BytePtrFromString(clientSecret) + accessTokenPtr, _ := syscall.BytePtrFromString(accessToken) + logisticsIdPtr, _ := syscall.BytePtrFromString(logisticsId) + orderSnPtr, _ := syscall.BytePtrFromString(orderSn) + orderStatePtr, _ := syscall.BytePtrFromString(orderState) + waybillNoPtr, _ := syscall.BytePtrFromString(waybillNo) + info, _, err := proc.Call( + uintptr(unsafe.Pointer(clientIdPtr)), + uintptr(unsafe.Pointer(clientSecretPtr)), + uintptr(unsafe.Pointer(accessTokenPtr)), + uintptr(unsafe.Pointer(logisticsIdPtr)), + uintptr(unsafe.Pointer(orderSnPtr)), + uintptr(unsafe.Pointer(orderStatePtr)), + uintptr(unsafe.Pointer(waybillNoPtr)), + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 PddGoodsOuterCatMappingGet 失败: %v", err) + } + return m.cStr(info), nil +} + +func main() { + clientId := "203c5a7ba8bd4b8488d5e26f93052642" + clientSecret := "892ffaa86e12b7a3d8d2942b669d9aa520ad8179" + accessToken := "bd96218bb2a146779701506dc1e5e5c478692539" + //outerCatId := "15543" + //outerCatName := "书籍/杂志/报纸" + //outerGoodsName := "书籍医家金鉴 妇产科学卷" + //logisticsId := 0 + //orderSn := "" + //orderState := "" + //waybillNo := "" + //logisticsCompany := "德邦" + + // 初始化 + //dll, err := InitPddDLL() + //if err != nil { + // fmt.Println(err) + //} + + // 类目预测 + //info, err := dll.PddGoodsOuterCatMappingGet(clientId, clientSecret, accessToken, outerCatId, outerCatName, outerGoodsName) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(info) + + // 快递公司查看 + //get, err := dll.PddLogisticsCompaniesGet(clientId, clientSecret) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(get) + //var logisticsResponse LogisticsResponse + //if err := json.Unmarshal([]byte(get), &logisticsResponse); err != nil { + // fmt.Println(err) + //} + // + //var company string + //var available int + //var code string + //for _, logisticsCompanies := range logisticsResponse.LogisticsCompaniesGetResponse.LogisticsCompanies { + // if strings.Contains(logisticsCompanies.LogisticsCompany, logisticsCompany) { + // company = logisticsCompanies.LogisticsCompany + // logisticsId = logisticsCompanies.ID + // available = logisticsCompanies.Available + // code = logisticsCompanies.Code + // break + // } + //} + //fmt.Println("快递公司名称: ", company) + //fmt.Println("快递公司编码: ", logisticsId) + //fmt.Println("是否有效: ", available) + //fmt.Println("物流公司代码: ", code) + + //file := "D:\\isbn_images\\result\\9780007935192.jpg" + //open, err := os.Open(file) + //if err != nil { + // fmt.Println(err) + //} + //defer open.Close() + ////base := filepath.Base(file) + //// 商品图片上传接口 + //upload, err := pddGoodsImgUpload(clientId, clientSecret, accessToken, file) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(upload) + + //get, err := pddLogisticsCompaniesGet(clientId, clientSecret) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(get) + + // 脱敏 + + jsonStr := `[{"data_tag":"251229-272441044622514","encrypted_data":"~AgAAAAPlscEH0psOJAEXpTdsLOWvDJ9bB7IEjIoqNfiDhhJR9NHOxsdZ+PEFluSSCngCikoDU+CP/sSXZJ92ic7+PdNlJNLA7g/6VUMDWF6RvjW9IeRN+lKNarsjWDQR~0~"}]` + + //var records []DataList + //err := json.Unmarshal([]byte(jsonStr), &records) + //if err != nil { + // log.Fatal("解析JSON失败:", err) + //} + + batch, err := pddOpenDecryptMaskBatch(clientId, clientSecret, accessToken, jsonStr) + if err != nil { + fmt.Println(err) + } + fmt.Println(batch) + +} + +type DataList struct { + DataTag string `json:"data_tag"` + EncryptedData string `json:"encrypted_data"` +} diff --git a/proxy/dll/proxy.dll b/proxy/dll/proxy.dll index 4e745fb..0a317ec 100644 Binary files a/proxy/dll/proxy.dll and b/proxy/dll/proxy.dll differ diff --git a/proxy/dll/proxy.h b/proxy/dll/proxy.h index dc81637..45dcab0 100644 --- a/proxy/dll/proxy.h +++ b/proxy/dll/proxy.h @@ -22,7 +22,8 @@ extern const char *_GoStringPtr(_GoString_ s); #line 3 "proxy.go" - #include + +#include #line 1 "cgo-generated-wrapper" @@ -87,35 +88,35 @@ extern "C" { #endif -// 导出函数:获取代理健康状态(用于调试) +// GetProxyHealth 导出函数:获取代理健康状态(用于调试) // extern __declspec(dllexport) char* GetProxyHealth(void); -// 导出函数:代理类型管理器 +// ProxyTypeManager 导出函数:代理类型管理器 // extern __declspec(dllexport) char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); -// 导出函数:查询机器码 +// GetMachineCode 导出函数:查询机器码 // extern __declspec(dllexport) char* GetMachineCode(char* tailCardSecret); -// 导出函数:充值卡密 +// RechargeCard 导出函数:充值卡密 // extern __declspec(dllexport) char* RechargeCard(char* tailCardSecret, char* machineCode); -// 导出函数:获取代理服务器列表 +// GetProxies 导出函数:获取代理服务器列表 // extern __declspec(dllexport) char* GetProxies(char* machineCode); -// 导出函数:检查卡密是否过期 +// CheckTailCardSecretExpired 导出函数:检查卡密是否过期 // extern __declspec(dllexport) char* CheckTailCardSecretExpired(char* tailCardSecret); -// 导出函数:初始化代理管理器 +// InitProxyManager 导出函数:初始化代理管理器 // extern __declspec(dllexport) char* InitProxyManager(char* serversJson, char* username, char* password, char* tailCardSecret, char* proxyType); -// 导出函数:释放C字符串内存 +// FreeCString 导出函数:释放C字符串内存 // extern __declspec(dllexport) void FreeCString(char* str); diff --git a/proxy/proxy.go b/proxy/proxy.go index 8c5bd47..2210e67 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -20,11 +20,11 @@ import ( // 代理类型常量 const ( - CalfElephantProxyType = "CALF_ELEPHANT_PROXY" - TailProxyType = "TAIL_PROXY" + CalfElephantProxyType = "CALF_ELEPHANT_PROXY" // 小象代理类型 + TailProxyType = "TAIL_PROXY" // 尾巴代理类型 ) -// 代理服务器列表 +// 小象代理服务器列表 var ( servers = []string{ "http-dynamic.xiaoxiangdaili.com", @@ -32,15 +32,16 @@ var ( "http-dynamic-S03.xiaoxiangdaili.com", "http-dynamic-S04.xiaoxiangdaili.com", } + // 互斥锁,用于保护全局随机数生成器的并发访问 randMutex sync.Mutex - globalRand *rand.Rand - + globalRand *rand.Rand // 全局随机数生成器 // 代理健康状态管理 + // 代理健康状态映射表,key为代理主机名,value为健康状态 proxyHealthMaps = make(map[string]*ProxyHealth) proxyHealthMutex sync.RWMutex ) -// ProxyManager 代理管理器结构体 +// ProxyManager 代理管理器结构体,用于管理代理配置信息 type ProxyManager struct { servers []string `json:"servers"` // 代理服务器列表 username string `json:"username"` // 代理账号 @@ -49,7 +50,7 @@ type ProxyManager struct { proxyType string `json:"proxy_type"` // 代理类型 CALF_ELEPHANT_PROXY/TAIL_PROXY } -// 代理健康状态 +// 代理健康状态结构体,用于跟踪代理的健康状况 type ProxyHealth struct { SuccessCount int // 成功次数 FailCount int // 失败次数 @@ -58,39 +59,46 @@ type ProxyHealth struct { IsHealthy bool // 是否健康 } +// 初始化函数,在程序启动时自动执行 func init() { - // 创建全局的随机数生成器 + // 创建全局的随机数生成器,使用当前时间作为种子 globalRand = rand.New(rand.NewSource(time.Now().UnixNano())) } -// 获取代理URL +// 获取代理URL,代理类型管理器,根据代理类型构建不同的代理URL func proxyTypeManager(proxyType, username, password, machineCode string) (string, error) { switch proxyType { case CalfElephantProxyType: + // 小象代理:使用用户名和密码构建代理URL return buildCalfElephantProxyURL(username, password) case TailProxyType: + // 尾巴代理:使用机器码构建代理URL return buildTailProxyURL(machineCode) default: + // 不支持的代理类型,返回错误 return "", fmt.Errorf("不支持的代理类型: %s", proxyType) } } // 构建小象代理URL func buildCalfElephantProxyURL(username, password string) (string, error) { + // 随机选择一个代理服务器 server := randomServer() + // 构建代理URL格式:http://用户名:密码@服务器:端口 proxyURL := fmt.Sprintf("http://%s:%s@%s:%d", - url.QueryEscape(username), - url.QueryEscape(password), - server, - 10030) + url.QueryEscape(username), // URL编码用户名,防止特殊字符问题 + url.QueryEscape(password), // URL编码密码,防止特殊字符问题 + server, // 服务器地址 + 10030) // 固定端口号 // 检测代理可用性 if err := checkProxyHealth(proxyURL); err != nil { + // 代理检测失败,记录警告日志 log.Printf("[WARN] 代理 %s 检测失败: %v", server, err) // 尝试下一个代理服务器 return tryNextCalfElephantProxy(username, password, server) } - + // 代理检测成功,记录信息日志 log.Printf("[INFO] 使用小象代理: %s", server) return proxyURL, nil } @@ -104,35 +112,39 @@ func tryNextCalfElephantProxy(username, password, failedServer string) (string, availableServers = append(availableServers, server) } } - + // 如果没有可用的服务器,返回错误 if len(availableServers) == 0 { return "", fmt.Errorf("所有小象代理服务器都不可用") } // 随机尝试可用服务器 for _, server := range shuffleServers(availableServers) { + // 构建代理URL proxyURL := fmt.Sprintf("http://%s:%s@%s:%d", url.QueryEscape(username), url.QueryEscape(password), server, 10030) - + // 检测代理可用性 if err := checkProxyHealth(proxyURL); err == nil { log.Printf("[INFO] 切换到可用代理: %s", server) return proxyURL, nil } + // 代理不可用,记录警告日志 log.Printf("[WARN] 代理 %s 检测失败", server) } - + // 所有服务器都检测失败,返回错误 return "", fmt.Errorf("所有可用的小象代理服务器都检测失败") } // 构建内置代理URL func buildTailProxyURL(machineCode string) (string, error) { + // 获取代理列表 proxies, err := getProxies(machineCode) if err != nil { return "", err } + // 检查是否获取到有效代理 if len(proxies) == 0 { return "", fmt.Errorf("未获取到有效代理") } @@ -140,6 +152,7 @@ func buildTailProxyURL(machineCode string) (string, error) { // 过滤并选择健康的代理 healthyProxies := filterHealthyProxies(proxies) if len(healthyProxies) > 0 { + // 从健康代理中随机选择一个 proxyURL := "http://" + randomElement(healthyProxies) log.Printf("[INFO] 使用健康尾巴代理: %s", proxyURL) return proxyURL, nil @@ -150,15 +163,16 @@ func buildTailProxyURL(machineCode string) (string, error) { return findWorkingTailProxy(proxies) } -// 过滤健康代理 +// 过滤健康代理,返回当前健康的代理列表 func filterHealthyProxies(proxies []string) []string { + // 获取读锁,允许多个goroutine同时读取 proxyHealthMutex.RLock() - defer proxyHealthMutex.RUnlock() + defer proxyHealthMutex.RUnlock() // 确保函数结束时释放锁 var healthy []string for _, proxy := range proxies { if health, exists := proxyHealthMaps[proxy]; exists && health.IsHealthy { - // 检查是否在最近检查过(5分钟内) + // 检查是否在最近检查过(5分钟内),避免使用过时的健康状态 if time.Since(health.LastCheck) < 5*time.Minute { healthy = append(healthy, proxy) } @@ -177,69 +191,73 @@ func findWorkingTailProxy(proxies []string) (string, error) { proxy string err error } - + // 创建带缓冲的通道,用于收集检测结果 ch := make(chan proxyResult, len(shuffledProxies)) - var wg sync.WaitGroup + var wg sync.WaitGroup // 等待组,用于等待所有检测goroutine完成 - // 限制并发数 + // 限制并发数,避免同时检测太多代理导致网络拥塞 sem := make(chan struct{}, 5) - + // 并发检测每个代理 for _, proxy := range shuffledProxies { - wg.Add(1) + wg.Add(1) // 增加等待组计数 go func(p string) { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() + defer wg.Done() // 函数结束时减少等待组计数 + sem <- struct{}{} // 获取信号量,限制并发数 + defer func() { <-sem }() // 释放信号量 proxyURL := "http://" + p err := checkProxyHealth(proxyURL) + // 将检测结果发送到通道 ch <- proxyResult{proxy: p, err: err} }(proxy) } - + // 等待所有检测goroutine完成 wg.Wait() - close(ch) + close(ch) // 关闭通道,表示不会再有数据发送 - // 收集结果 + // 收集结果,查找可用的代理 var workingProxies []string for result := range ch { if result.err == nil { + // 代理可用,添加到工作代理列表 workingProxies = append(workingProxies, result.proxy) // 更新健康状态 updateProxyHealth(result.proxy, true, 0) } else { + // 代理不可用,更新健康状态 updateProxyHealth(result.proxy, false, 0) } } - + // 如果有可用的代理,随机选择一个 if len(workingProxies) > 0 { selected := randomElement(workingProxies) log.Printf("[INFO] 找到可用代理: %s (共 %d 个可用)", selected, len(workingProxies)) return "http://" + selected, nil } - + // 所有代理都不可用,返回错误 return "", fmt.Errorf("所有尾巴代理都不可用") } -// 检测代理健康状态 +// 检测代理健康状态,通过访问测试网站验证代理可用性 func checkProxyHealth(proxyURL string) error { - start := time.Now() - + start := time.Now() // 记录开始时间,用于计算响应时间 + // 创建HTTP请求,设置代理和超时 req := gorequest.New().Proxy(proxyURL).Timeout(10 * time.Second) + // 发送GET请求到测试网站 resp, _, errs := req.Get("https://shop.kongfz.com/").End() - responseTime := time.Since(start) - + responseTime := time.Since(start) // 计算响应时间 + // 检查请求错误 if len(errs) > 0 { updateProxyHealth(proxyURL, false, responseTime) return fmt.Errorf("代理连接失败: %v", errs) } - + // 检查HTTP状态码 if resp.StatusCode != 200 { updateProxyHealth(proxyURL, false, responseTime) return fmt.Errorf("代理响应状态码错误: %d", resp.StatusCode) } - + // 代理检测成功 updateProxyHealth(proxyURL, true, responseTime) log.Printf("[DEBUG] 代理 %s 检测成功, 响应时间: %v", getProxyHost(proxyURL), responseTime) return nil @@ -247,25 +265,29 @@ func checkProxyHealth(proxyURL string) error { // 更新代理健康状态 func updateProxyHealth(proxyURL string, success bool, responseTime time.Duration) { + // 获取写锁,确保更新操作的互斥性 proxyHealthMutex.Lock() - defer proxyHealthMutex.Unlock() - + defer proxyHealthMutex.Unlock() // 确保函数结束时释放锁 + // 获取代理主机名作为键 host := getProxyHost(proxyURL) + // 如果代理健康状态不存在,创建新的 if _, exists := proxyHealthMaps[host]; !exists { proxyHealthMaps[host] = &ProxyHealth{} } health := proxyHealthMaps[host] - health.LastCheck = time.Now() + health.LastCheck = time.Now() // 更新最后检查时间 if success { - health.SuccessCount++ - health.FailCount = 0 - health.ResponseTime = responseTime - health.IsHealthy = true + // 成功的情况 + health.SuccessCount++ // 增加成功次数 + health.FailCount = 0 // 重置失败次数 + health.ResponseTime = responseTime // 记录响应时间 + health.IsHealthy = true // 标记为健康 } else { - health.FailCount++ - health.SuccessCount = 0 + // 失败的情况 + health.FailCount++ // 增加失败次数 + health.SuccessCount = 0 // 重置成功次数 // 连续失败3次标记为不健康 if health.FailCount >= 3 { health.IsHealthy = false @@ -275,10 +297,11 @@ func updateProxyHealth(proxyURL string, success bool, responseTime time.Duration // 获取代理主机名 func getProxyHost(proxyURL string) string { + // 去除协议前缀 if strings.HasPrefix(proxyURL, "http://") { proxyURL = proxyURL[7:] } - // 去除认证信息 + // 去除认证信息(用户名:密码@部分) if atIndex := strings.Index(proxyURL, "@"); atIndex != -1 { proxyURL = proxyURL[atIndex+1:] } @@ -296,18 +319,19 @@ func randomServer() string { // 线程安全的随机元素选择 func randomElement(slice []string) string { - randMutex.Lock() - defer randMutex.Unlock() - return slice[globalRand.Intn(len(slice))] + randMutex.Lock() // 获取互斥锁 + defer randMutex.Unlock() // 确保函数结束时释放锁 + return slice[globalRand.Intn(len(slice))] // 随机选择一个元素 } // 打乱服务器顺序 func shuffleServers(servers []string) []string { randMutex.Lock() defer randMutex.Unlock() - + // 创建服务器副本,避免修改原始切片 shuffled := make([]string, len(servers)) copy(shuffled, servers) + // 使用Fisher-Yates算法打乱顺序 globalRand.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) @@ -316,52 +340,59 @@ func shuffleServers(servers []string) []string { // 检查卡密是否过期 func checkTailCardSecretExpired(tailCardSecret string) (bool, error) { + // 获取机器码信息 code, err := getMachineCode(tailCardSecret) if err != nil { return false, fmt.Errorf("请求错误: %v", err) } + // 解析过期时间字符串 targetTime, err := time.Parse("2006-01-02 15:04:05", code.Data.IpExpTime) if err != nil { return false, fmt.Errorf("时间格式错误: %v", err) } currentTime := time.Now() - // 卡密日期在当前日期之后 + // 卡密日期在当前日期之后表示有效 if targetTime.After(currentTime) { return true, nil } else { + // 卡密已过期 return false, fmt.Errorf("卡密已经过期!过期时间: %v", targetTime) } } // 定义响应结构体 type getMachineCodeResp struct { - Code int `json:"code"` - Message string `json:"message"` + Code int `json:"code"` // 状态码 + Message string `json:"message"` // 消息 Data struct { - MachineCode string `json:"machine_code"` - IpExpTime string `json:"ip_exp_time"` - IpThread int `json:"ip_thread"` - IpCardCode string `json:"ip_card_code"` + MachineCode string `json:"machine_code"` // 机器码 + IpExpTime string `json:"ip_exp_time"` // IP过期时间 + IpThread int `json:"ip_thread"` // IP线程数 + IpCardCode string `json:"ip_card_code"` // IP卡密 } `json:"data"` } // 查询机器码 func getMachineCode(tailCardSecret string) (*getMachineCodeResp, error) { url := "http://114.66.2.223:7842/api/proxies/ip_show" + // 构建请求数据 data := map[string]interface{}{ "ip_card_code": tailCardSecret, "agent_id": 9999, } + // 发送POST请求 _, body, errs := gorequest.New().Post(url).Send(data).End() if len(errs) > 0 { return nil, fmt.Errorf("查询机器码失败: %v", errs) } + // 解析响应 var resp getMachineCodeResp if err := json.Unmarshal([]byte(body), &resp); err != nil { return nil, fmt.Errorf("解析响应失败: %v", err) } - + // 处理不同的响应码 if resp.Code == 201 { + // 需要充值卡密 machineCode, err := rechargeCard(tailCardSecret, resp.Data.MachineCode) if err != nil { return nil, err @@ -369,8 +400,10 @@ func getMachineCode(tailCardSecret string) (*getMachineCodeResp, error) { resp.Data.MachineCode = machineCode return &resp, nil } else if resp.Code == 200 { + // 成功获取机器码 return &resp, nil } else { + // 其他错误 return nil, fmt.Errorf("查询机器码失败: %s", resp.Message) } } @@ -378,15 +411,18 @@ func getMachineCode(tailCardSecret string) (*getMachineCodeResp, error) { // 充值卡密 func rechargeCard(tailCardSecret, machineCode string) (string, error) { url := "http://114.66.2.223:7842/api/proxies/ip_recharge" + // 构建请求数据 data := map[string]interface{}{ "machine_code": machineCode, "ip_card_code": tailCardSecret, "agent_id": 9999, } + // 发送POST请求 _, body, errs := gorequest.New().Post(url).Send(data).End() if len(errs) > 0 { return "", fmt.Errorf("充值卡密失败: %v", errs) } + // 解析响应 var resp struct { Code int `json:"code"` // 状态码 Message string `json:"message"` // 返回消息 @@ -399,6 +435,8 @@ func rechargeCard(tailCardSecret, machineCode string) (string, error) { if err := json.Unmarshal([]byte(body), &resp); err != nil { return "", fmt.Errorf("解析响应失败: %v", err) } + + // 检查响应状态 if resp.Code != 200 { return "", fmt.Errorf("充值卡密失败: %s", resp.Message) } @@ -411,16 +449,18 @@ func getProxies(machineCode string) ([]string, error) { // 生成签名 sign := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("9999%s9999", machineCode)))) + // 构建请求URL GetProxiesUrl := fmt.Sprintf("http://114.66.2.223:7842/api/proxies/get_proxy?machine_code=%s&sign=%s&agent_id=9999", machineCode, sign) + // 发送GET请求 req := gorequest.New().Get(GetProxiesUrl).Timeout(20 * time.Second) _, body, errs := req.End() if len(errs) > 0 { return nil, fmt.Errorf("获取代理失败: %v", errs) } - // 检查是否为JSON错误响应 + // 检查是否为JSON错误响应(响应以{开头,以}结尾) if strings.HasPrefix(strings.TrimSpace(body), "{") && strings.HasSuffix(strings.TrimSpace(body), "}") { // 尝试解析为JSON错误响应 var errorResp struct { @@ -432,15 +472,17 @@ func getProxies(machineCode string) ([]string, error) { } } - // 解析响应 + // 解析响应(每行一个代理地址) lines := strings.Split(strings.TrimSpace(body), "\n") var proxies []string for _, line := range lines { line = strings.TrimSpace(line) + // 过滤空行和JSON格式的行 if line != "" && !strings.HasPrefix(line, "{") { proxies = append(proxies, line) } } + // 检查是否获取到有效代理 if len(proxies) == 0 { return nil, fmt.Errorf("未获取到有效代理") } @@ -456,13 +498,13 @@ func initProxy() { // =================== C 导出函数 ======================= -// 导出函数:获取代理健康状态(用于调试) +// GetProxyHealth 导出函数:获取代理健康状态(用于调试) // //export GetProxyHealth func GetProxyHealth() *C.char { proxyHealthMutex.RLock() defer proxyHealthMutex.RUnlock() - + // 构建健康信息映射 healthInfo := make(map[string]interface{}) for proxy, health := range proxyHealthMaps { healthInfo[proxy] = map[string]interface{}{ @@ -473,7 +515,7 @@ func GetProxyHealth() *C.char { "is_healthy": health.IsHealthy, } } - + // 序列化为JSON jsonData, err := json.Marshal(healthInfo) if err != nil { return C.CString(fmt.Sprintf(`{"error": "序列化健康信息失败: %v"}`, err)) @@ -482,17 +524,18 @@ func GetProxyHealth() *C.char { return C.CString(string(jsonData)) } -// 导出函数:代理类型管理器 +// ProxyTypeManager 导出函数:代理类型管理器 // //export ProxyTypeManager func ProxyTypeManager(proxyType, username, password, machineCode *C.char) *C.char { + // C字符串转换为Go字符串 goProxyType := C.GoString(proxyType) goUsername := C.GoString(username) goPassword := C.GoString(password) goMachineCode := C.GoString(machineCode) log.Printf("[DEBUG] 代理类型管理器调用: type=%s", goProxyType) - + // 调用代理类型管理器 proxyURL, err := proxyTypeManager(goProxyType, goUsername, goPassword, goMachineCode) if err != nil { errorMsg := fmt.Sprintf("ERROR: %v", err) @@ -504,7 +547,7 @@ func ProxyTypeManager(proxyType, username, password, machineCode *C.char) *C.cha return C.CString(proxyURL) } -// 导出函数:查询机器码 +// GetMachineCode 导出函数:查询机器码 // //export GetMachineCode func GetMachineCode(tailCardSecret *C.char) *C.char { @@ -530,7 +573,7 @@ func GetMachineCode(tailCardSecret *C.char) *C.char { return C.CString(string(jsonData)) } -// 导出函数:充值卡密 +// RechargeCard 导出函数:充值卡密 // //export RechargeCard func RechargeCard(tailCardSecret, machineCode *C.char) *C.char { @@ -563,7 +606,7 @@ func RechargeCard(tailCardSecret, machineCode *C.char) *C.char { return C.CString(string(jsonData)) } -// 导出函数:获取代理服务器列表 +// GetProxies 导出函数:获取代理服务器列表 // //export GetProxies func GetProxies(machineCode *C.char) *C.char { @@ -595,7 +638,7 @@ func GetProxies(machineCode *C.char) *C.char { return C.CString(string(jsonData)) } -// 导出函数:检查卡密是否过期 +// CheckTailCardSecretExpired 导出函数:检查卡密是否过期 // //export CheckTailCardSecretExpired func CheckTailCardSecretExpired(tailCardSecret *C.char) *C.char { @@ -634,7 +677,7 @@ func CheckTailCardSecretExpired(tailCardSecret *C.char) *C.char { return C.CString(string(jsonData)) } -// 导出函数:初始化代理管理器 +// InitProxyManager 导出函数:初始化代理管理器 // //export InitProxyManager func InitProxyManager(serversJson, username, password, tailCardSecret, proxyType *C.char) *C.char { @@ -685,7 +728,7 @@ func InitProxyManager(serversJson, username, password, tailCardSecret, proxyType return C.CString(string(jsonData)) } -// 导出函数:释放C字符串内存 +// FreeCString 导出函数:释放C字符串内存 // //export FreeCString func FreeCString(str *C.char) { diff --git a/xy/dll/xy.dll b/xy/dll/xy.dll new file mode 100644 index 0000000..8250f8a Binary files /dev/null and b/xy/dll/xy.dll differ diff --git a/xy/dll/xy.h b/xy/dll/xy.h new file mode 100644 index 0000000..8e5b4a7 --- /dev/null +++ b/xy/dll/xy.h @@ -0,0 +1,114 @@ +/* Code generated by cmd/cgo; DO NOT EDIT. */ + +/* package command-line-arguments */ + + +#line 1 "cgo-builtin-export-prolog" + +#include + +#ifndef GO_CGO_EXPORT_PROLOGUE_H +#define GO_CGO_EXPORT_PROLOGUE_H + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef struct { const char *p; ptrdiff_t n; } _GoString_; +extern size_t _GoStringLen(_GoString_ s); +extern const char *_GoStringPtr(_GoString_ s); +#endif + +#endif + +/* Start of preamble from import "C" comments. */ + + +#line 3 "main.go" + +#include +#include + +#line 1 "cgo-generated-wrapper" + + +/* End of preamble from import "C" comments. */ + + +/* Start of boilerplate cgo prologue. */ +#line 1 "cgo-gcc-export-header-prolog" + +#ifndef GO_CGO_PROLOGUE_H +#define GO_CGO_PROLOGUE_H + +typedef signed char GoInt8; +typedef unsigned char GoUint8; +typedef short GoInt16; +typedef unsigned short GoUint16; +typedef int GoInt32; +typedef unsigned int GoUint32; +typedef long long GoInt64; +typedef unsigned long long GoUint64; +typedef GoInt64 GoInt; +typedef GoUint64 GoUint; +typedef size_t GoUintptr; +typedef float GoFloat32; +typedef double GoFloat64; +#ifdef _MSC_VER +#if !defined(__cplusplus) || _MSVC_LANG <= 201402L +#include +typedef _Fcomplex GoComplex64; +typedef _Dcomplex GoComplex128; +#else +#include +typedef std::complex GoComplex64; +typedef std::complex GoComplex128; +#endif +#else +typedef float _Complex GoComplex64; +typedef double _Complex GoComplex128; +#endif + +/* + static assertion to make sure the file is being used on architecture + at least with matching size of GoInt. +*/ +typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1]; + +#ifndef GO_CGO_GOSTRING_TYPEDEF +typedef _GoString_ GoString; +#endif +typedef void *GoMap; +typedef void *GoChan; +typedef struct { void *t; void *v; } GoInterface; +typedef struct { void *data; GoInt len; GoInt cap; } GoSlice; + +#endif + +/* End of boilerplate cgo prologue. */ + +#ifdef __cplusplus +extern "C" { +#endif + +extern __declspec(dllexport) void FreeCString(char* str); +extern __declspec(dllexport) char* StartServer(char* configFile); +extern __declspec(dllexport) char* StopServer(void); +extern __declspec(dllexport) char* GetServerStatus(void); +extern __declspec(dllexport) char* GetServerAddress(void); +extern __declspec(dllexport) char* ReloadConfig(char* configFile); +extern __declspec(dllexport) char* ExecuteGoodsCreat(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteOtherTypeGoods(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteGoodsPublish(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteGoodsDownShelf(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteGoodsFlash(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteGoodsEditPrice(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteGoodsEditStock(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteSelectGoodsListPrice(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteSelectShopListPrice(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteOpenExpressCompanies(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteOpenOrderShip(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteXyOrderSynchronization(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteGetGoodsDetail(char* bodyJson, char* configFile); +extern __declspec(dllexport) char* ExecuteCountOuterId(char* bodyJson, char* configFile); + +#ifdef __cplusplus +} +#endif