diff --git a/.gitignore b/.gitignore index aaadf73..6904d23 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,6 @@ go.work.sum # env file .env -# Editor/IDE -# .idea/ -# .vscode/ +Editor/IDE +.idea/ +.vscode/ diff --git a/bookImage.go b/bookImage.go new file mode 100644 index 0000000..a053100 --- /dev/null +++ b/bookImage.go @@ -0,0 +1,202 @@ +package main + +import ( + "database/sql" + "fmt" + + _ "github.com/go-sql-driver/mysql" +) + +// 全局变量 +var ( + // 数据库连接配置 + DBUsername = "root" // 用户名 + DBPassword = "Long6166@@" // 密码 + DBHost = "nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com" // 主机 + DBPort = 26247 // 端口 + DBDataBase = "book_center" // 数据库 + DBCharset = "utf8mb4" // 字符集 + + //连接池配置 + MaxOpenConns = 20 + MaxIdleConns = 10 +) + +// 连接数据库 +func connectDB() (*sql.DB, error) { + // 数据源名称格式 + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", + DBUsername, + DBPassword, + DBHost, + DBPort, + DBDataBase, + DBCharset) + // 连接数据库 + db, err := sql.Open("mysql", dsn) + if err != nil { + return nil, fmt.Errorf("打开数据库连接失败: %v", err) + } + // 设置连接池参数 + db.SetMaxOpenConns(MaxOpenConns) // 最大打开连接数 + db.SetMaxIdleConns(MaxIdleConns) // 最大空闲连接数 + // 测试链接 + err = db.Ping() + if err != nil { + return nil, fmt.Errorf("数据库连接测试失败: %v", err) + } + return db, nil +} + +func selectBookIsbn(db *sql.DB, bookId int) error { + //// 执行语句 + //query := `SELECT isbn, MAX(create_time) as max_create_time + //FROM shop_goods_rejection + //WHERE isbn IS NOT NULL + //GROUP BY isbn` + // + //exec, err := db.Exec(query) + //fmt.Println(exec, err) + return nil +} + +//func main() { +//db, err := connectDB() +//if err != nil { +// log.Fatal("数据库连接失败:", err) +//} +// +//books, err := GetAllCategoryBooks() +//if err != nil { +// fmt.Printf("", err) +//} +//if len(books) == 0 { +// fmt.Printf("数组是空!") +//} +//// 查询数据库中不存在的ISBN +//missingISBNs, err := findMissingISBNs(db, books) +//if err != nil { +// log.Fatal("查询失败:", err) +//} +//log.Printf("数据库中不存在的ISBN数量: %d", len(missingISBNs)) +//fmt.Println("数据库中不存在的ISBN数组:") +//for i, isbn := range missingISBNs { +// fmt.Printf("%d. %s\n", i+1, isbn) +//} +// +//missingISBNsString := strings.Join(missingISBNs, "\n") +// +//// 这里您可以将 missingISBNs 数组用于后续处理 +//// 比如保存到文件、插入到另一个表等 +//fmt.Printf("\n新的missingISBNs字符串: %v\n", missingISBNsString) +//} + +// 查询数据库中不存在的ISBN +func findMissingISBNs(db *sql.DB, books []string) ([]string, error) { + if len(books) == 0 { + return []string{}, nil + } + + // 查询数据库中存在的ISBN + existingISBNs, err := queryExistingISBNs(db, books) + if err != nil { + return nil, err + } + + // 创建现有ISBN的映射,用于快速查找 + existingMap := make(map[string]bool) + for _, isbn := range existingISBNs { + existingMap[isbn] = true + } + + // 创建新数组,存储数据库中不存在的ISBN + missingISBNs := make([]string, 0) + for _, isbn := range books { + if !existingMap[isbn] { + missingISBNs = append(missingISBNs, isbn) + } + } + + return missingISBNs, nil +} + +// 查询数据库中存在的ISBN +func queryExistingISBNs(db *sql.DB, isbns []string) ([]string, error) { + if len(isbns) == 0 { + return []string{}, nil + } + + // 分批处理,避免SQL语句过长 + batchSize := 1000 + allExistingISBNs := make([]string, 0) + + for i := 0; i < len(isbns); i += batchSize { + end := i + batchSize + if end > len(isbns) { + end = len(isbns) + } + + batchISBNs := isbns[i:end] + batchExisting, err := queryBatchISBNs(db, batchISBNs) + if err != nil { + return nil, err + } + allExistingISBNs = append(allExistingISBNs, batchExisting...) + } + + return allExistingISBNs, nil +} + +// 批量查询ISBN +func queryBatchISBNs(db *sql.DB, isbns []string) ([]string, error) { + if len(isbns) == 0 { + return []string{}, nil + } + // 构建IN查询的占位符 + placeholders := make([]string, len(isbns)) + args := make([]interface{}, len(isbns)) + for i, isbn := range isbns { + placeholders[i] = "?" + args[i] = isbn + } + query := fmt.Sprintf(` + SELECT isbn + FROM book_center + WHERE isbn IN (%s) + AND del_flag = 0 + `, joinPlaceholders(placeholders, ",")) + + rows, err := db.Query(query, args...) + if err != nil { + return nil, fmt.Errorf("查询数据库失败: %v", err) + } + defer rows.Close() + + var existingISBNs []string + for rows.Next() { + var isbn string + if err := rows.Scan(&isbn); err != nil { + return nil, fmt.Errorf("扫描结果失败: %v", err) + } + existingISBNs = append(existingISBNs, isbn) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("遍历结果失败: %v", err) + } + + return existingISBNs, nil +} + +// joinPlaceholders 连接占位符 +func joinPlaceholders(placeholders []string, sep string) string { + if len(placeholders) == 0 { + return "" + } + + result := placeholders[0] + for i := 1; i < len(placeholders); i++ { + result += sep + placeholders[i] + } + return result +} diff --git a/bookNameError.go b/bookNameError.go new file mode 100644 index 0000000..97cd39f --- /dev/null +++ b/bookNameError.go @@ -0,0 +1,299 @@ +package main + +import ( + "database/sql" + "fmt" + "regexp" + "strings" + + _ "github.com/go-sql-driver/mysql" +) + +// func main() { +// // 构建数据库连接字符串 +// dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", +// "root", "Long6166@@", "nj-cynosdbmysql-grp-1v6vxn5f.sql.tencentcdb.com", "26247", "book_center") +// // 连接数据库 +// db, err := sql.Open("mysql", dsn) +// if err != nil { +// log.Fatal("数据库连接失败:", err) +// } +// defer db.Close() +// +// // 测试数据库连接 +// err = db.Ping() +// if err != nil { +// log.Fatal("数据库连接测试失败:", err) +// } +// fmt.Println("数据库连接成功") +// +// // 查询包含"影印版"或"(影印版)"的书名 +// books, err := queryBooksWithCopyVersion(db) +// if err != nil { +// log.Fatal("查询失败:", err) +// } +// +// fmt.Printf("找到 %d 本包含'影印版'或'(影印版)'的书\n", len(books)) +// +// if len(books) == 0 { +// fmt.Println("没有找到需要更新的图书") +// return +// } +// +// // 更新书名,删除"影印版"和"(影印版)" +// updatedCount, err := updateBookNames(db, books) +// if err != nil { +// log.Fatal("更新失败:", err) +// } +// +// fmt.Printf("成功更新 %d 本书的书名\n", updatedCount) +// } +// +// Book 结构体对应 book_center 表 +type Book struct { + ID int64 + BookName string + ISBN string +} + +// queryBooksWithCopyVersion 查询包含"影印版"或"(影印版)"的书名 +func queryBooksWithCopyVersion(db *sql.DB) ([]Book, error) { + query := ` + SELECT id, book_name, isbn + FROM book_center + WHERE book_name LIKE '%影印版%' + AND del_flag = 0 + ` + + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + var books []Book + for rows.Next() { + var book Book + err := rows.Scan(&book.ID, &book.BookName, &book.ISBN) + if err != nil { + return nil, err + } + books = append(books, book) + } + + return books, nil +} + +// cleanBookName 清理书名,删除所有影印版相关文字 +func cleanBookName(bookName string) string { + // 定义需要删除的模式 + patterns := []string{ + "(影印版)", // 全角括号 + "(影印版)", // 半角括号 + "影印版", // 无括号 + "【影印版】", // 方头括号 + "[影印版]", // 方括号 + } + newName := bookName + + // 逐个删除模式 + for _, pattern := range patterns { + newName = strings.ReplaceAll(newName, pattern, "") + } + + // 删除多余的空格 + newName = strings.TrimSpace(newName) + newName = regexp.MustCompile(`\s+`).ReplaceAllString(newName, " ") + + return newName +} + +// updateBookNames 更新书名,删除所有影印版相关文字 +func updateBookNames(db *sql.DB, books []Book) (int, error) { + tx, err := db.Begin() + if err != nil { + return 0, err + } + defer tx.Rollback() + + updateStmt, err := tx.Prepare(` + UPDATE book_center + SET book_name = ?, update_time = UNIX_TIMESTAMP() + WHERE id = ? AND isbn = ? + `) + if err != nil { + return 0, err + } + defer updateStmt.Close() + + updatedCount := 0 + for _, book := range books { + // 清理书名 + newBookName := cleanBookName(book.BookName) + + // 如果书名没有变化,跳过更新 + if newBookName == book.BookName { + fmt.Printf("跳过: 书名无变化 (ID: %d, ISBN: %s)\n", book.ID, book.ISBN) + continue + } + + // 如果书名超过字段长度限制,截断 + if len(newBookName) > 400 { + newBookName = newBookName[:400] + fmt.Printf("警告: 书名超长,已截断 (ID: %d)\n", book.ID) + } + + result, err := updateStmt.Exec(newBookName, book.ID, book.ISBN) + if err != nil { + fmt.Printf("更新失败 (ID: %d): %v\n", book.ID, err) + continue + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + fmt.Printf("获取影响行数失败 (ID: %d): %v\n", book.ID, err) + continue + } + + if rowsAffected > 0 { + updatedCount++ + fmt.Printf("更新成功: ID=%d, ISBN=%s\n", book.ID, book.ISBN) + fmt.Printf(" 原书名: %s\n", book.BookName) + fmt.Printf(" 新书名: %s\n", newBookName) + fmt.Println(" ---") + } + } + + err = tx.Commit() + if err != nil { + return 0, err + } + + return updatedCount, nil +} + +// 没用了 +//// 获取销量榜所有分类中图书的isbn +//func GetAllCategoryBooks() ([]string, error) { +// // 1. 获取分类列表 +// categories, err := GetSalesBookInfo() +// if err != nil { +// return nil, fmt.Errorf("获取分类列表失败: %v", err) +// } +// // 初始化返回的ISBN数组 +// var allISBNs []string +// +// for i, category := range categories { +// fmt.Printf("\n=== 正在处理分类 %d/%d: %s (ID: %d) ===\n", i+1, len(categories), category.Key, category.Value) +// // 根据分类ID获取图书详情 +// bookDetails, err := GetBookDetailsByCategory(category.Value) +// if err != nil { +// fmt.Printf("获取分类 %s 的图书信息失败: %v\n", category.Key, err) +// continue +// } +// // 将当前分类的图书数据添加到结果map中 +// if bookDetails != nil && bookDetails.Result.Data != nil { +// for _, book := range bookDetails.Result.Data { +// if book.Isbn != "" { +// allISBNs = append(allISBNs, book.Isbn) +// fmt.Printf("添加到ISBN列表: %s\n", book.Isbn) +// } +// } +// fmt.Printf("分类 %s 获取到 %d 本图书\n", category.Key, len(bookDetails.Result.Data)) +// } else { +// fmt.Printf("分类 %s 没有获取到图书数据\n", category.Key) +// } +// time.Sleep(500 * time.Microsecond) +// } +// // 去重处理(可选) +// allISBNs = removeDuplicateISBNs(allISBNs) +// // 打印统计信息 +// fmt.Printf("\n=== 总计: %d 个分类, %d 个唯一ISBN ===\n", len(categories), len(allISBNs)) +// return allISBNs, nil +//} +// +//// 获取销量榜的图书信息 +//func GetSalesBookInfo() ([]SalesCategory, error) { +// // 构建请求URL +// url := "https://item.kongfz.com/api/Pc/getSellWellCatList" +// // 创建HTTP客户端 +// client := &http.Client{ +// Timeout: 30 * time.Second, +// } +// // 创建请求 +// req, err := http.NewRequest("GET", url, nil) +// if err != nil { +// return nil, fmt.Errorf("创建请求失败: %v", err) +// } +// // 设置请求头,模拟浏览器请求 +// req.Header.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") +// req.Header.Set("Accept", "application/json, text/plain, */*") +// req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") +// req.Header.Set("Referer", "https://item.kongfz.com/") +// // 发送请求 +// resp, err := client.Do(req) +// if err != nil { +// return nil, fmt.Errorf("请求失败: %v", err) +// } +// defer resp.Body.Close() +// // 检查HTTP状态码 +// if resp.StatusCode != http.StatusOK { +// return nil, fmt.Errorf("HTTP错误: %s", resp.Status) +// } +// // 读取响应体 +// body, err := io.ReadAll(resp.Body) +// if err != nil { +// return nil, fmt.Errorf("读取响应失败: %v", err) +// } +// // 解析JSON响应 +// var salesCategoryResponse SalesCategoryResponse +// err = json.Unmarshal(body, &salesCategoryResponse) +// if err != nil { +// return nil, fmt.Errorf("解析JSON失败: %v", err) +// } +// return salesCategoryResponse.Result, nil +//} +// +//// 根据分类ID获取图书详情 +//func GetBookDetailsByCategory(catId int) (*BookDetailResponse, error) { +// // 构建请求URL +// url := fmt.Sprintf("https://item.kongfz.com/api/pc/getSellWellListDetail?page=1&pageSize=100&timeRank=2&catId=%d", catId) +// // 创建HTTP客户端 +// client := &http.Client{ +// Timeout: 30 * time.Second, +// } +// // 创建请求 +// req, err := http.NewRequest("GET", url, nil) +// if err != nil { +// return nil, fmt.Errorf("创建请求失败: %v", err) +// } +// // 设置请求头,模拟浏览器请求 +// req.Header.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") +// req.Header.Set("Accept", "application/json, text/plain, */*") +// req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") +// req.Header.Set("Referer", "https://item.kongfz.com/") +// // 发送请求 +// resp, err := client.Do(req) +// if err != nil { +// return nil, fmt.Errorf("请求失败: %v", err) +// } +// defer resp.Body.Close() +// // 检查HTTP状态码 +// if resp.StatusCode != http.StatusOK { +// return nil, fmt.Errorf("HTTP错误: %s", resp.Status) +// } +// // 读取响应体 +// body, err := io.ReadAll(resp.Body) +// if err != nil { +// return nil, fmt.Errorf("读取响应失败: %v", err) +// } +// var bookDetailResponse BookDetailResponse +// err = json.Unmarshal(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) +// } +// return &bookDetailResponse, nil +//} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..f4e6718 --- /dev/null +++ b/config.ini @@ -0,0 +1,59 @@ +# 应用配置 +[app] +# 最大重试次数 +max_retry_times = 3 +# 间隔休眠时间 +rate_limit_delay = 500ms +# 线程数 +size = 5 +# 默认用户代理 +default_user_agent = Mozilla/5.0 + +# 代理配置 +[proxy] +# 小象代理服务器列表(逗号分隔) +servers = http-dynamic.xiaoxiangdaili.com,http-dynamic-S02.xiaoxiangdaili.com,http-dynamic-S03.xiaoxiangdaili.com,http-dynamic-S04.xiaoxiangdaili.com +# 代理账号 +username = 1297757178467602432 +# 代理密码 +password = QgQBvP7f +# 尾巴代理机器码 +tail_machine_code = b7bf22a237ec692f13fcc2c43ee63252 +# 尾巴代理卡密 +tail_card_key = DL_20_YK_1920acb2129844c2aabade3896560a9b +# config配置dll +proxy_file_path = dll/proxyConfig.dll + +# API接口配置 +[api] +# 登录接口地址 +login_url = https://login.kongfz.com/Pc/Login/account +# 图书搜索接口地址 +book_search_url = https://search.kongfz.com/pc-gw/search-web/client/pc/bookLib/keyword/list +# 产品搜索接口地址 +product_search_url = https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list + +# 服务器配置 +[server] +# 服务监听端口 +port = 9999 + +# 图片配置 +[image] +# 图片保存目录 +target_dir = /file/goods_img/ +# 图片保存目录(本地测试路径) +; target_dir = ./images/ +# 图片访问域名 +domain = book.center.image.buzhiyushu.cn + +# 数据库配置 +[database] +# 数据库用户名 +username = newAdmin +# 数据库密码 +password = bYPp8SbBe5F7nz2i +# 数据库主机 +host = 146.56.227.42:3306 +# 数据库名称 +name = newadmin \ No newline at end of file diff --git a/dll/kongfz.h b/dll/kongfz.h new file mode 100644 index 0000000..03820fd --- /dev/null +++ b/dll/kongfz.h @@ -0,0 +1,118 @@ +/* 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 + + // proxyConfig.dll 函数声明 + extern char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); + extern void FreeCString(char* str); + +#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 + + +// 获取商品图片(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetImageByIsbn(char* token, char* isbn, char* proxy, int isLiveImage, int isReturnMsg); + +// 获取商品列表通过店铺ID(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetGoodsListMsgByShopId(int shopId, char* proxy, int isImage, char* sortType, char* sort, float priceMin, float priceMax, int pageNum, int returnNum); + +// 获取商品信息通过商品详情链接(带有0ut的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetGoodsMsgByDetailUrl(char* detailUrl, char* proxy); + +// 获取销量榜商品列表(带有Out的都非官放标准接口) +// +extern __declspec(dllexport) char* OutGetTopGoodsListMsg(int catId, char* proxy); +extern __declspec(dllexport) char* Initialize(char* configJSON); + +// 导出函数:释放C字符串内存 +// +extern __declspec(dllexport) void FreeCString(char* str); + +#ifdef __cplusplus +} +#endif diff --git a/dll/proxyConfig.h b/dll/proxyConfig.h new file mode 100644 index 0000000..171e8ae --- /dev/null +++ b/dll/proxyConfig.h @@ -0,0 +1,104 @@ +/* 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 "proxyConfig.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 + + +// 导出函数:获取代理健康状态(用于调试) +// +extern __declspec(dllexport) char* GetProxyHealth(void); + +// 导出函数:代理类型管理器 +// +extern __declspec(dllexport) char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); + +// 导出函数:释放C字符串内存 +// +extern __declspec(dllexport) void FreeCString(char* str); + +#ifdef __cplusplus +} +#endif diff --git a/dll/test.h b/dll/test.h new file mode 100644 index 0000000..427c73f --- /dev/null +++ b/dll/test.h @@ -0,0 +1,120 @@ +/* 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 "kfztp.go" + +#include + +// proxyConfig.dll 函数声明 +extern char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); +extern void FreeCString(char* str); + +#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 + + +// 获取商品图片(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetImageByIsbn(char* isbn, char* proxy, _Bool isLiveImage, _Bool isReturnMsg); + +// 获取商品列表通过店铺ID(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetGoodsListMsgByShopId(int shopId, char* proxy, _Bool isImage, char* sortType, char* sort, float priceMin, float priceMax, int pageNum, int returnNum); + +// 获取商品信息通过商品详情链接(带有0ut的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetGoodsMsgByDetailUrl(char* detailUrl, char* proxy); + +// 获取销量榜商品列表(带有Out的都非官放标准接口) +// +extern __declspec(dllexport) char* OutGetTopGoodsListMsg(int catId, char* proxy); +extern __declspec(dllexport) char* GetKFZSPTImageURL(char* proxyType, char* username, char* password, char* machineCode, char* isbn); +extern __declspec(dllexport) char* GetKFZGTImageURL(char* proxyType, char* username, char* password, char* machineCode, char* isbn); +extern __declspec(dllexport) char* Initialize(char* configJSON); + +// 导出函数:释放C字符串内存 +// +extern __declspec(dllexport) void FreeCString(char* str); + +#ifdef __cplusplus +} +#endif diff --git a/dll/xkongfz.dll~ b/dll/xkongfz.dll~ new file mode 100644 index 0000000..67d8165 Binary files /dev/null and b/dll/xkongfz.dll~ differ diff --git a/dll/xkongfz.h b/dll/xkongfz.h new file mode 100644 index 0000000..5c450e9 --- /dev/null +++ b/dll/xkongfz.h @@ -0,0 +1,122 @@ +/* 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 + + // proxyConfig.dll 函数声明 + extern char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); + extern void FreeCString(char* str); + +#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 + + +// 登录(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutLogin(char* username, char* password); + +// 获取商品图片(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetImageByIsbn(char* token, char* isbn, char* proxy, int isLiveImage, int isReturnMsg); + +// 获取商品列表通过店铺ID(带有Out的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetGoodsListMsgByShopId(int shopId, char* proxy, int isImage, char* sortType, char* sort, float priceMin, float priceMax, int pageNum, int returnNum); + +// 获取商品信息通过商品详情链接(带有0ut的都非官方标准接口) +// +extern __declspec(dllexport) char* OutGetGoodsMsgByDetailUrl(char* detailUrl, char* proxy); + +// 获取销量榜商品列表(带有Out的都非官放标准接口) +// +extern __declspec(dllexport) char* OutGetTopGoodsListMsg(int catId, char* proxy); +extern __declspec(dllexport) char* Initialize(char* configJSON); + +// 导出函数:释放C字符串内存 +// +extern __declspec(dllexport) void FreeCString(char* str); + +#ifdef __cplusplus +} +#endif diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e5bc59c --- /dev/null +++ b/go.mod @@ -0,0 +1,29 @@ +module kfzgw-info + +go 1.25 + +require ( + github.com/PuerkitoBio/goquery v1.10.3 + github.com/chromedp/chromedp v0.14.2 + github.com/go-ini/ini v1.67.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/parnurzeal/gorequest v0.3.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect + github.com/elazarl/goproxy v1.7.2 // indirect + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // 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/moul/http2curl v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/smartystreets/goconvey v1.8.1 // indirect + github.com/stretchr/testify v1.10.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sys v0.34.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7267768 --- /dev/null +++ b/go.sum @@ -0,0 +1,124 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= +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/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/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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +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-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= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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/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/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= +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/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/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/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +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/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= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/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= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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/iniConfigUtil.go b/iniConfigUtil.go new file mode 100644 index 0000000..05e3f54 --- /dev/null +++ b/iniConfigUtil.go @@ -0,0 +1,378 @@ +// Package utils 提供配置文件加载工具,支持从INI文件加载配置到结构体 +// 支持类型: int, string, bool, float, time.Duration 及其切片类型 +// 特性: +// - 支持默认值标签 `default` +// - 支持INI路径标签 `ini:"section.key"` +// - 支持时间单位转换标签 `multiplier` +// - 自动处理切片类型(逗号分隔) +package main + +import ( + "log" + "os" + "reflect" + "regexp" + "strconv" + "time" + + "github.com/go-ini/ini" +) + +// LoadConfig 从INI文件加载配置到结构体 +// 参数: +// +// configPtr: 指向配置结构体的指针(必须是结构体指针) +// filename: INI配置文件的路径 +// +// 返回: +// +// error: 加载成功返回nil,失败返回ConfigError详细错误信息 +// +// 用法说明: +// 1. 定义配置结构体,使用标签声明INI映射关系和默认值 +// 2. 调用LoadConfig(&config, "config.ini") +// 3. 检查错误并应用配置 +// +// 示例结构体: +// +// type AppConfig struct { +// Port int `ini:"server.port" default:"8080"` +// Timeout time.Duration `ini:"server.timeout" multiplier:"1s"` +// Features []string `ini:"server.features"` +// } +func LoadConfig(configPtr interface{}, filename string) error { + // 验证输入必须是指针 + if reflect.TypeOf(configPtr).Kind() != reflect.Ptr { + return &ConfigError{Message: "configPtr必须是指向结构体的指针"} // 返回错误 + } + + // 验证输入必须指向结构体 + configValue := reflect.ValueOf(configPtr).Elem() + // 确保输入是结构体 + if configValue.Kind() != reflect.Struct { + return &ConfigError{Message: "configPtr必须指向结构体"} // 返回错误 + } + + // 设置默认值(如果结构体有默认值) + setDefaultValues(configValue) + + // 检查配置文件是否存在 + if _, err := os.Stat(filename); os.IsNotExist(err) { + log.Printf("配置文件不存在: %s, 使用默认值", filename) // 打印信息 + return nil // 返回错误 + } + + // 加载INI文件 + iniCfg, err := ini.Load(filename) + // 处理错误 + if err != nil { + return &ConfigError{Message: "加载配置文件失败", Cause: err} // 返回错误 + } + + // 映射配置到结构体 + if err := mapConfig(iniCfg, configValue); err != nil { + return err // 返回错误 + } + + // 处理特殊类型(如time.Duration) + processSpecialTypes(configValue) + + // 返回成功 + return nil +} + +// setDefaultValues 设置结构体字段的默认值 +// 遍历结构体字段,检测`default`标签并设置初始值 +// setDefaultValues 设置结构体字段的默认值 +func setDefaultValues(configValue reflect.Value) { + // 获取结构体类型 + configType := configValue.Type() + + // 遍历字段 + for i := 0; i < configType.NumField(); i++ { + // 获取字段信息 + field := configType.Field(i) + // 获取字段值 + fieldValue := configValue.Field(i) + + // 如果字段是结构体,递归处理 + if fieldValue.Kind() == reflect.Struct { + setDefaultValues(fieldValue) + continue + } + + // 如果字段已经设置了值,跳过 + if !fieldValue.IsZero() { + continue // 跳过 + } + + // 检查是否有默认值标签 + if defaultValue, ok := field.Tag.Lookup("default"); ok { + setValueFromString(fieldValue, defaultValue) // 设置字段值 + } + } +} + +// mapConfig 将INI配置映射到结构体字段 +// 解析`ini`标签获取section和key,读取对应配置值 +func mapConfig(iniCfg *ini.File, configValue reflect.Value) error { + // 获取结构体类型 + configType := configValue.Type() + + // 遍历字段 + for i := 0; i < configType.NumField(); i++ { + field := configType.Field(i) // 获取字段信息 + fieldValue := configValue.Field(i) // 获取字段值 + + // 如果字段是结构体,递归处理 + if fieldValue.Kind() == reflect.Struct { + if err := mapConfig(iniCfg, fieldValue); err != nil { + return err + } + continue + } + + // 获取INI标签 + iniTag, ok := field.Tag.Lookup("ini") + // 如果没有INI标签,跳过这个字段 + if !ok || iniTag == "" { + continue // 跳过 + } + + // 解析INI键名(支持section.key格式) + sectionName, keyName := parseIniTag(iniTag) + + // 获取INI值 + section, err := iniCfg.GetSection(sectionName) + // 如果section不存在 + if err != nil { + continue // 跳过这个字段 + } + + // 获取INI键值 + key, err := section.GetKey(keyName) + // 如果key不存在 + if err != nil { + continue //跳过这个字段 + } + + // 设置结构体字段值 + if err := setFieldValue(fieldValue, key); err != nil { + // 返回错误 + return &ConfigError{ + Message: "设置字段值失败", // 返回错误信息 + Cause: err, // 返回错误原因 + Field: field.Name, // 返回字段名称 + Tag: iniTag, // 返回标签 + } + } + } + + return nil +} + +// parseIniTag 解析INI标签格式 +// 输入: "section.key" 格式的字符串 +// 返回: (section名称, key名称) +func parseIniTag(tag string) (section, key string) { + // 默认section + section = "DEFAULT" + // 默认key + key = tag + + // 检查是否有section前缀 + if parts := regexp.MustCompile(`^(\w+)\.(\w+)$`).FindStringSubmatch(tag); len(parts) == 3 { + section = parts[1] // 设置section + key = parts[2] // 设置key + } + + // 返回section和key + return section, key +} + +// setFieldValue 根据INI键值设置结构体字段值 +// 自动处理基础类型和time.Duration类型转换 +func setFieldValue(fieldValue reflect.Value, key *ini.Key) error { + // 检查字段类型 + switch fieldValue.Kind() { + case reflect.String: // 字符串类型 + fieldValue.SetString(key.String()) // 设置字段值 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 整数类型 + // 特殊处理time.Duration类型 + if fieldValue.Type() == reflect.TypeOf(time.Duration(0)) { + duration, err := time.ParseDuration(key.String()) // 解析字符串为time.Duration + // 处理错误 + if err != nil { + return err // 返回错误 + } + fieldValue.SetInt(int64(duration)) // 设置字段值 + } else { + intValue, err := key.Int() // 获取整数值 + // 处理错误 + if err != nil { + return err // 返回错误 + } + fieldValue.SetInt(int64(intValue)) // 设置字段值 + } + case reflect.Bool: // 布尔类型 + boolValue, err := key.Bool() // 获取布尔值 + // 处理错误 + if err != nil { + return err // 返回错误 + } + fieldValue.SetBool(boolValue) // 设置字段值 + case reflect.Float32, reflect.Float64: // 浮点类型 + floatValue, err := key.Float64() // 获取浮点值 + // 处理错误 + if err != nil { + return err // 返回错误 + } + fieldValue.SetFloat(floatValue) // 设置字段值 + case reflect.Slice: // 切片类型 + return setSliceValue(fieldValue, key) // 处理切片类型字段 + default: // 其他类型 + return &ConfigError{Message: "不支持的字段类型", FieldType: fieldValue.Type().String()} // 返回错误 + } + + // 返回成功 + return nil +} + +// setSliceValue 设置切片类型字段值 +// 将逗号分隔的字符串解析为指定类型的切片 +func setSliceValue(fieldValue reflect.Value, key *ini.Key) error { + // 获取切片元素类型 + sliceType := fieldValue.Type().Elem() + // 获取逗号分隔的值 + values := key.Strings(",") + + // 创建新切片 + slice := reflect.MakeSlice(fieldValue.Type(), len(values), len(values)) + + // 遍历值 + for i, val := range values { + elemValue := reflect.New(sliceType).Elem() // 创建新元素 + + // 根据切片元素类型设置值 + switch sliceType.Kind() { + case reflect.String: // 字符串类型 + elemValue.SetString(val) // 设置字段值 + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: // 整数类型 + intVal, err := strconv.ParseInt(val, 10, 64) // 解析字符串为整数 + // 处理错误 + if err != nil { + return err // 返回错误 + } + elemValue.SetInt(intVal) // 设置字段值 + case reflect.Float32, reflect.Float64: // 浮点类型 + floatVal, err := strconv.ParseFloat(val, 64) // 解析字符串为浮点数 + // 处理错误 + if err != nil { + return err // 返回错误 + } + elemValue.SetFloat(floatVal) // 设置字段值 + case reflect.Bool: // 布尔类型 + boolVal, err := strconv.ParseBool(val) // 解析字符串为布尔值 + // 处理错误 + if err != nil { + return err // 返回错误 + } + elemValue.SetBool(boolVal) // 设置字段值 + default: + return &ConfigError{Message: "不支持的切片元素类型", FieldType: sliceType.String()} // 返回错误 + } + + slice.Index(i).Set(elemValue) // 设置切片元素 + } + + // 设置切片字段值 + fieldValue.Set(slice) + // 返回成功 + return nil +} + +// setValueFromString 从字符串解析值到结构体字段(用于默认值) +// 支持基础类型转换,不处理复杂类型 +func setValueFromString(fieldValue reflect.Value, valueStr string) { + switch fieldValue.Kind() { + case reflect.String: + fieldValue.SetString(valueStr) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if intValue, err := strconv.ParseInt(valueStr, 10, 64); err == nil { + fieldValue.SetInt(intValue) + } + case reflect.Bool: + if boolValue, err := strconv.ParseBool(valueStr); err == nil { + fieldValue.SetBool(boolValue) + } + case reflect.Float32, reflect.Float64: + if floatValue, err := strconv.ParseFloat(valueStr, 64); err == nil { + fieldValue.SetFloat(floatValue) + } + case reflect.Slice: + // 切片类型需要特殊处理,这里简化处理 + default: + // 保留原panic调用,但修改提示信息 + panic("未处理的默认值类型") + } +} + +// processSpecialTypes 处理特殊类型转换 +// 当前支持time.Duration的倍数转换(使用multiplier标签) +// processSpecialTypes 处理特殊类型转换 +func processSpecialTypes(configValue reflect.Value) { + // 获取结构体类型 + configType := configValue.Type() + + // 遍历结构体字段 + for i := 0; i < configType.NumField(); i++ { + field := configType.Field(i) // 获取字段 + fieldValue := configValue.Field(i) // 获取字段值 + + // 如果字段是结构体,递归处理 + if fieldValue.Kind() == reflect.Struct { + processSpecialTypes(fieldValue) + continue + } + + // 处理time.Duration类型的倍数转换 + if fieldValue.Type() == reflect.TypeOf(time.Duration(0)) { + // 检查是否存在multiplier标签 + if multiplier, ok := field.Tag.Lookup("multiplier"); ok { + // 解析multiplier标签 + if mult, err := time.ParseDuration(multiplier); err == nil { + duration := time.Duration(fieldValue.Int()) // 将字段值转换为time.Duration + fieldValue.SetInt(int64(duration * mult)) // 将字段值设置为转换后的时间 + } + } + } + } +} + +// ConfigError 自定义配置错误类型 +// 包含错误原因、字段信息和原始错误 +type ConfigError struct { + Message string + Cause error + Field string + FieldType string + Tag string +} + +// Error 实现error接口,提供详细错误信息 +func (e *ConfigError) Error() string { + msg := "配置错误: " + e.Message + if e.Field != "" { + msg += " [字段: " + e.Field + "]" + } + if e.FieldType != "" { + msg += " [类型: " + e.FieldType + "]" + } + if e.Tag != "" { + msg += " [标签: " + e.Tag + "]" + } + if e.Cause != nil { + msg += " - " + e.Cause.Error() + } + return msg +} diff --git a/kfztp.go b/kfztp.go new file mode 100644 index 0000000..39d76f2 --- /dev/null +++ b/kfztp.go @@ -0,0 +1,2011 @@ +package main + +import "C" + +///* +//#include +// +//// proxyConfig.dll 函数声明 +//extern char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); +//extern void FreeCString(char* str); +//*/ +//import "C" +//import ( +// "context" +// "encoding/json" +// "fmt" +// "io" +// "log" +// "net/http" +// "os" +// "path/filepath" +// "regexp" +// "strconv" +// "strings" +// "sync" +// "syscall" +// "time" +// "unsafe" +// +// "github.com/PuerkitoBio/goquery" +// "github.com/chromedp/chromedp" +// "github.com/parnurzeal/gorequest" +//) +// +//const ( +// UseProxy = "proxy" // 使用代理 +// NotProxy = "direct" // 不用代理代理 +//) +// +//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"` +// } `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"` +// } `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"` +// } `ini:"proxy" json:"proxy"` +//} +// +//// 条目详情结构体 +//type BookInfo struct { +// BookName string `json:"book_name"` // 书名 +// Author string `json:"author"` // 作者 +// Publisher string `json:"publisher"` // 出版社 +// ISBN string `json:"isbn"` // ISBN +// PublicationTime int64 `json:"publication_time"` // 出版时间 +// Edition string `json:"edition"` // 版次 +// PrintTime string `json:"print_time"` // 印刷时间 +// FixPrice string `json:"fix_price"` // 定价 +// BindingLayout string `json:"binding_layout"` // 装帧 +// Format string `json:"format"` // 开本 +// Paper string `json:"paper"` // 纸张 +// Pages string `json:"pages"` // 页数 +// Wordage string `json:"wordage"` // 字数 +// Languages string `json:"languages"` // 语种 +// Era string `json:"era"` // 年代 +// EngravingMethod string `json:"engraving_method"` // 刻印方式 +// Dimensions string `json:"dimensions"` // 尺寸 +// VolumeNumber string `json:"volume_number"` // 册数 +// BookPic string `json:"book_pic"` // 图书封面图(官图) +// BookPicS string `json:"book_pic_s"` // 图书封面图(实拍图) +// SellingPrice string `json:"selling_price"` // 售价 +// Condition string `json:"condition"` // 品相 +// ExpressDeliveryFee string `json:"express_delivery_fee"` // 快递费 +// Editor string `json:"editor"` // 编辑 +// Category string `json:"category"` // 分类 +// BuyCount string `json:"buy_count"` // 买过 +// SellCount string `json:"sell_count"` // 在卖 +// Content string `json:"content"` // 内容 +// Mid int64 `json:"mid"` // 商家id +// ItemId int64 `json:"item_id"` // 商品id +// ShopId int64 `json:"shop_id"` // 店铺id +//} +// +//// API响应结构 +//type APIResponse struct { +// Success bool `json:"success"` +// Message string `json:"message,omitempty"` +// GoodsNum string `json:"goods_num,omitempty"` +// PNum string `json:"pnum,omitempty"` +// Data interface{} `json:"data,omitempty"` +// Error string `json:"error,omitempty"` +//} +// +//// 全局变量 +//var ( +// cf Config // 配置信息 +// tailProxyMu sync.Mutex // 互斥锁 +// proxyFailCount int +// +// // 全局代理管理器 +// globalProxyManager *ProxyConfigManager +// proxyManagerOnce sync.Once +// proxyManagerInitErr error +//) +// +//// ProductInfo 商品信息结构 +//type ProductInfo struct { +// ItemID string `json:"itemId"` +// BookName string `json:"bookName"` +// Price string `json:"price"` +// ShippingFee string `json:"shippingFee"` +//} +// +//// ProductResponse 响应结构 +//type ProductResponse struct { +// Success bool `json:"success"` +// Message string `json:"message,omitempty"` +// Data []ProductInfo `json:"data,omitempty"` +//} +// +//// 并行获取详情的结果结构 +//type DetailResult struct { +// URL string +// Doc *goquery.Document +// Error error +// Index int +//} +// +//// 第一阶段:收集基本信息并识别需要详情的项目 +//type BookItem struct { +// Book BookInfo +// Selection *goquery.Selection +// HasDetail bool +// DetailURL string +// Index int +//} +// +//// 销量榜目录分类列表响应结构体 +//type SalesCategoryResponse struct { +// Status bool `json:"status"` +// Result []SalesCategory `json:"result"` +// ErrMessage string `json:"errMessage"` +// ErrCode int `json:"errCode"` +//} +// +//// 分类项结构体 +//type SalesCategory struct { +// Key string `json:"key"` +// Value int `json:"value"` +//} +// +//// 图书详情响应结构体 +//type BookDetailResponse struct { +// Status bool `json:"status"` +// Result BookList `json:"result"` +// ErrMessage string `json:"errMessage"` +// ErrCode int `json:"errCode"` +//} +// +//// 图书列表结构体 +//type BookList struct { +// Current int `json:"current"` +// Data []BookInformation `json:"data"` +// Total int `json:"total"` +//} +// +//// 图书信息结构体 +//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"` +//} +// +//// 作者信息结构体 +//type AuthorInfo struct { +// Name string `json:"name"` +// OriName string `json:"oriName"` +// Nationality string `json:"nationality"` +// Role string `json:"role"` +// Url string `json:"url"` +//} +// +//// 商品链接结构体 +//type ItemUrls struct { +// AppUrl string `json:"appUrl"` +// MUrl string `json:"mUrl"` +// MiniUrl string `json:"miniUrl"` +// PcUrl string `json:"pcUrl"` +//} +// +//// ProxyConfigManager 代理配置DLL管理器 +//type ProxyConfigManager struct { +// dll *syscall.DLL +//} +// +//func NewProxyConfigManager(dllPath string) (*ProxyConfigManager, error) { +// dll, err := syscall.LoadDLL(dllPath) +// if err != nil { +// return nil, fmt.Errorf("加载代理配置DLL失败: %v", err) +// } +// return &ProxyConfigManager{dll: dll}, nil +//} +// +//func (m *ProxyConfigManager) Close() { +// if m.dll != nil { +// m.dll.Release() +// } +//} +// +//// ProxyTypeManager 调用代理类型管理器 +//func (m *ProxyConfigManager) ProxyTypeManager(proxyType, username, password, machineCode string) (string, error) { +// proc, err := m.dll.FindProc("ProxyTypeManager") +// if err != nil { +// return "", fmt.Errorf("找不到函数 ProxyTypeManager: %v", err) +// } +// +// // 准备参数 +// proxyTypePtr, _ := syscall.BytePtrFromString(proxyType) +// usernamePtr, _ := syscall.BytePtrFromString(username) +// passwordPtr, _ := syscall.BytePtrFromString(password) +// machineCodePtr, _ := syscall.BytePtrFromString(machineCode) +// +// // 调用函数 +// r1, _, err := proc.Call( +// uintptr(unsafe.Pointer(proxyTypePtr)), +// uintptr(unsafe.Pointer(usernamePtr)), +// uintptr(unsafe.Pointer(passwordPtr)), +// uintptr(unsafe.Pointer(machineCodePtr)), +// ) +// +// if err != nil && err.Error() != "The operation completed successfully." { +// return "", fmt.Errorf("调用 ProxyTypeManager 失败: %v", err) +// } +// +// // 转换结果 +// result := (*byte)(unsafe.Pointer(r1)) +// var resultBytes []byte +// for i := 0; ; i++ { +// bytePtr := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) +// if *bytePtr == 0 { +// break +// } +// resultBytes = append(resultBytes, *bytePtr) +// } +// +// // 释放内存 +// freeProc, _ := m.dll.FindProc("FreeCString") +// if freeProc != nil { +// freeProc.Call(r1) +// } +// +// return string(resultBytes), nil +//} +// +//// 获取销量榜所有分类中图书的isbn +//func GetAllCategoryBooks() ([]string, error) { +// // 1. 获取分类列表 +// categories, err := GetSalesBookInfo() +// if err != nil { +// return nil, fmt.Errorf("获取分类列表失败: %v", err) +// } +// // 初始化返回的ISBN数组 +// var allISBNs []string +// +// for i, category := range categories { +// fmt.Printf("\n=== 正在处理分类 %d/%d: %s (ID: %d) ===\n", i+1, len(categories), category.Key, category.Value) +// // 根据分类ID获取图书详情 +// bookDetails, err := GetBookDetailsByCategory(category.Value) +// if err != nil { +// fmt.Printf("获取分类 %s 的图书信息失败: %v\n", category.Key, err) +// continue +// } +// // 将当前分类的图书数据添加到结果map中 +// if bookDetails != nil && bookDetails.Result.Data != nil { +// for _, book := range bookDetails.Result.Data { +// if book.Isbn != "" { +// allISBNs = append(allISBNs, book.Isbn) +// fmt.Printf("添加到ISBN列表: %s\n", book.Isbn) +// } +// } +// fmt.Printf("分类 %s 获取到 %d 本图书\n", category.Key, len(bookDetails.Result.Data)) +// } else { +// fmt.Printf("分类 %s 没有获取到图书数据\n", category.Key) +// } +// time.Sleep(500 * time.Microsecond) +// } +// // 去重处理(可选) +// allISBNs = removeDuplicateISBNs(allISBNs) +// // 打印统计信息 +// fmt.Printf("\n=== 总计: %d 个分类, %d 个唯一ISBN ===\n", len(categories), len(allISBNs)) +// return allISBNs, nil +//} +// +//// 辅助函数:去除重复的ISBN +//func removeDuplicateISBNs(isbns []string) []string { +// seen := make(map[string]bool) +// var result []string +// +// for _, isbn := range isbns { +// if !seen[isbn] { +// seen[isbn] = true +// result = append(result, isbn) +// } +// } +// +// return result +//} +// +//// 获取销量榜的图书信息 +//func GetSalesBookInfo() ([]SalesCategory, error) { +// // 构建请求URL +// url := "https://item.kongfz.com/api/Pc/getSellWellCatList" +// // 创建HTTP客户端 +// client := &http.Client{ +// Timeout: 30 * time.Second, +// } +// // 创建请求 +// req, err := http.NewRequest("GET", url, nil) +// if err != nil { +// return nil, fmt.Errorf("创建请求失败: %v", err) +// } +// // 设置请求头,模拟浏览器请求 +// req.Header.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") +// req.Header.Set("Accept", "application/json, text/plain, */*") +// req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") +// req.Header.Set("Referer", "https://item.kongfz.com/") +// // 发送请求 +// resp, err := client.Do(req) +// if err != nil { +// return nil, fmt.Errorf("请求失败: %v", err) +// } +// defer resp.Body.Close() +// // 检查HTTP状态码 +// if resp.StatusCode != http.StatusOK { +// return nil, fmt.Errorf("HTTP错误: %s", resp.Status) +// } +// // 读取响应体 +// body, err := io.ReadAll(resp.Body) +// if err != nil { +// return nil, fmt.Errorf("读取响应失败: %v", err) +// } +// // 解析JSON响应 +// var salesCategoryResponse SalesCategoryResponse +// err = json.Unmarshal(body, &salesCategoryResponse) +// if err != nil { +// return nil, fmt.Errorf("解析JSON失败: %v", err) +// } +// return salesCategoryResponse.Result, nil +//} +// +//// 根据分类ID获取图书详情 +//func GetBookDetailsByCategory(catId int) (*BookDetailResponse, error) { +// // 构建请求URL +// url := fmt.Sprintf("https://item.kongfz.com/api/pc/getSellWellListDetail?page=1&pageSize=100&timeRank=2&catId=%d", catId) +// // 创建HTTP客户端 +// client := &http.Client{ +// Timeout: 30 * time.Second, +// } +// // 创建请求 +// req, err := http.NewRequest("GET", url, nil) +// if err != nil { +// return nil, fmt.Errorf("创建请求失败: %v", err) +// } +// // 设置请求头,模拟浏览器请求 +// req.Header.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") +// req.Header.Set("Accept", "application/json, text/plain, */*") +// req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") +// req.Header.Set("Referer", "https://item.kongfz.com/") +// // 发送请求 +// resp, err := client.Do(req) +// if err != nil { +// return nil, fmt.Errorf("请求失败: %v", err) +// } +// defer resp.Body.Close() +// // 检查HTTP状态码 +// if resp.StatusCode != http.StatusOK { +// return nil, fmt.Errorf("HTTP错误: %s", resp.Status) +// } +// // 读取响应体 +// body, err := io.ReadAll(resp.Body) +// if err != nil { +// return nil, fmt.Errorf("读取响应失败: %v", err) +// } +// var bookDetailResponse BookDetailResponse +// err = json.Unmarshal(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) +// } +// return &bookDetailResponse, nil +//} +// +//// 通过店铺ID获取孔网实拍图和条目信息 +//func getKFZShopBookInfo(fetchMode, proxyType, username, password, machineCode, shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp string) (books []BookInfo, goodsNum string, pNum string, err error) { +// // 判断店铺ID +// if shopId == "" { +// return nil, "", "", fmt.Errorf("店铺编码为空!") +// } +// // 判断是否有图片,设置默认值0 +// if isImage == "" { +// isImage = "0" +// } else { +// _, isImageErr := strconv.Atoi(isImage) +// if isImageErr != nil { +// return nil, "", "", fmt.Errorf("是否有图片输入必须是数字!", isImageErr) +// } +// } +// // 判断一页图书数量,设置默认值100 +// if bookNum == "" { +// bookNum = "100" +// } else { +// isBookNum, bookNumErr := strconv.Atoi(bookNum) +// if bookNumErr != nil { +// return nil, "", "", fmt.Errorf("图书数量输入必须是数字!", bookNumErr) +// } +// if isBookNum < 0 && isBookNum > 100 { +// return nil, "", "", fmt.Errorf("图书数量输入错误:只能出入(0~100区间)") +// } +// } +// // 判断页数,设置默认值1 +// if pageNum == "" { +// pageNum = "1" +// } else { +// _, pageNumErr := strconv.Atoi(pageNum) +// if pageNumErr != nil { +// return nil, "", "", fmt.Errorf("页数输入必须是数字!", pageNumErr) +// } +// } +// // 判断排序类型,设置默认值sort +// if sortType == "" { +// sortType = "sort" +// } else { +// validSortTypes := map[string]bool{ +// "sort": true, +// "putDate": true, +// "newItem": true, +// "price": true, +// } +// if !validSortTypes[sortType] { +// return nil, "", "", fmt.Errorf("无效的排序类型: %s,可选值: sort, putDate, newItem, price", sortType) +// } +// } +// // 判断排序,设置默认值desc +// if sort == "" { +// sort = "desc" +// } else { +// validSorts := map[string]bool{ +// "desc": true, +// "asc": true, +// } +// if !validSorts[sort] { +// return nil, "", "", fmt.Errorf("无效的排序类型: %s,可选值: desc, asc", sort) +// } +// } +// // 判断价格下限,设置默认值0 +// if priceDown == "" { +// priceDown = "0" +// } else { +// _, priceDownErr := strconv.ParseFloat(priceDown, 64) +// if priceDownErr != nil { +// return nil, "", "", fmt.Errorf("价格输入必须是数字!", priceDownErr) +// } +// } +// // 判断价格上限,设置默认值0 +// if priceUp == "" { +// priceUp = "0" +// } else { +// _, priceUpErr := strconv.ParseFloat(priceUp, 64) +// if priceUpErr != nil { +// return nil, "", "", fmt.Errorf("价格输入必须是数字!", priceUpErr) +// } +// } +// // 调用的url +// url := fmt.Sprintf("https://shop.kongfz.com/%s/all/%s_%s_0_0_%s_%s_%s_%s_%s", +// shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp) +// log.Printf("调用的URL: %s", url) +// // 并行获取商品信息(使用优化后的Chromedp) +// response, err := FetchProductInfoWithChromedp(url) +// if err != nil { +// fmt.Printf("❌ 获取商品信息失败: %v", err) +// } +// // 发送请求 +// doc, err := fetchDocument(fetchMode, proxyType, username, password, machineCode, url) +// +// // 全部商品数量 +// num := doc.Find("div.crumbs-nav-main.clearfix").Find("span") +// if match := regexp.MustCompile(`\d+`).FindString(num.Text()); match != "" { +// goodsNum = match +// } +// // 商品页数 +// pg := doc.Find("li.pull-right.page_num").Find("span") +// _, split, found := strings.Cut(strings.TrimSpace(pg.Text()), "/") +// if found { +// pNum = split +// } else { +// log.Printf("未找到页数!") +// } +// +// infoDiv := doc.Find("div.list-content") +// if infoDiv.Length() > 0 { +// item := infoDiv.Find("div.item.clearfix") +// var bookItems []BookItem +// +// for i := 0; i < item.Length(); i++ { +// s := item.Eq(i) +// book := BookInfo{} +// // 书名 +// book.BookName = strings.TrimSpace(s.Find("div.title a.link").Text()) +// // 作者 +// book.Author = extractNormalText(s, "作者") +// // 出版社 +// book.Publisher = extractNormalText(s, "出版社") +// // 出版时间,提取出版时间并转换为时间戳 +// pubTimeStr := extractNormalText(s, "出版时间") +// if pubTimeStr != "" { +// // 尝试不同的时间格式 +// formats := []string{"2006-01", "2006-01-02", "2006"} +// for _, format := range formats { +// if t, err := time.Parse(format, pubTimeStr); err == nil { +// book.PublicationTime = t.Unix() +// break +// } +// } +// } +// // 提取ISBN +// book.ISBN = strings.TrimSpace(s.AttrOr("isbn", "")) +// // 版次 +// book.Edition = extractNormalText(s, "版次") +// // 印刷时间 +// book.PrintTime = extractNormalText(s, "印刷时间") +// // 装帧 +// book.BindingLayout = extractNormalText(s, "装帧") +// // 开本 +// book.Format = extractNormalText(s, "开本") +// // 纸张 +// book.Paper = extractNormalText(s, "纸张") +// // 字数 +// book.Wordage = extractNormalText(s, "字数") +// // 售价,提取价格(优先从元素属性获取) +// book.SellingPrice = s.AttrOr("price", "") +// if book.SellingPrice == "" { +// book.SellingPrice = strings.TrimSpace(s.Find("div.price span.bold").Text()) +// } +// // 提取图片 +// if img, exists := s.Find("div.item-img img").Attr("src"); exists { +// book.BookPicS = img +// } +// // 快递费 +// val, exists := s.Attr("itemid") +// for in, p := range response.Data { +// if exists { +// if val == p.ItemID { +// book.ExpressDeliveryFee = p.ShippingFee +// in = in + 1 +// } +// } +// } +// // 提取品相 +// book.Condition = strings.TrimSpace(s.Find("div.quality").Text()) +// +// // 检查是否需要获取详情 +// hasDetail := s.Find("div.zl-isbn-info").Length() > 0 +// detailURl := "" +// if hasDetail { +// detailURl = s.Find("a.img-box").AttrOr("href", "") +// } +// +// bookItems = append(bookItems, BookItem{ +// Book: book, +// Selection: s, +// HasDetail: hasDetail, +// DetailURL: detailURl, +// Index: i, +// }) +// } +// +// detailResults := fetchAllDetailsParallel(fetchMode, proxyType, username, password, machineCode, bookItems) +// books = mergeBookDetails(bookItems, detailResults) +// } +// return books, goodsNum, pNum, nil +//} +// +//// 并行获取所有详情 +//func fetchAllDetailsParallel(fetchMode, proxyType, username, password, machineCode string, bookItems []BookItem) map[string]*DetailResult { +// // 收集所有需要获取详情的URL +// var detailTasks []struct { +// URL string +// Index int +// } +// +// for _, item := range bookItems { +// if item.HasDetail && item.DetailURL != "" { +// detailTasks = append(detailTasks, struct { +// URL string +// Index int +// }{ +// URL: item.DetailURL, +// Index: item.Index, +// }) +// } +// } +// +// if len(detailTasks) == 0 { +// return make(map[string]*DetailResult) +// } +// +// type taskResult struct { +// URL string +// Doc *goquery.Document +// Error error +// Index int +// } +// +// ch := make(chan taskResult, len(detailTasks)) +// var wg sync.WaitGroup +// +// // 限制并发数,避免过多连接 +// sem := make(chan struct{}, 5) // 最大5个并发 +// +// for _, task := range detailTasks { +// wg.Add(1) +// go func(url string, index int) { +// defer wg.Done() +// sem <- struct{}{} // 获取信号量 +// defer func() { <-sem }() // 释放信号量 +// +// doc, err := fetchDocument(fetchMode, proxyType, username, password, machineCode, url) +// ch <- taskResult{ +// URL: url, +// Doc: doc, +// Error: err, +// Index: index, +// } +// }(task.URL, task.Index) +// } +// +// wg.Wait() +// close(ch) +// +// results := make(map[string]*DetailResult) +// for res := range ch { +// results[res.URL] = &DetailResult{ +// URL: res.URL, +// Doc: res.Doc, +// Error: res.Error, +// Index: res.Index, +// } +// } +// +// return results +//} +// +//// 获取Document文档(带重试机制) +//func fetchDocument(fetchMode, proxyType, username, password, machineCode, url string) (*goquery.Document, error) { +// log.Printf("调用的URL: %s", url) +// maxRetries := cf.App.MaxRetryTimes +// var detailsResp *http.Response +// var errors []error +// var detailsBody []byte +// for attempt := 0; attempt < maxRetries; attempt++ { +// if attempt > 0 { +// log.Printf("第 %d 次重试请求...", attempt) +// // 重试前等待,使用指数退避策略 +// waitTime := time.Duration(attempt*attempt) * cf.App.RateLimitDelay // 平方退避 +// log.Printf("等待 %v 后重试", waitTime) +// time.Sleep(waitTime) +// } +// if fetchMode == NotProxy { +// detailsResp, _, errors = gorequest.New(). +// Get(url). +// Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"). +// Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). +// Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). +// Timeout(120 * time.Second). +// End() +// } else if fetchMode == UseProxy { +// detailsReq := XxProxyRequest(proxyType, username, password, machineCode). +// Get(url). +// Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"). +// Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). +// Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). +// Set("Connection", "close") // 每次请求关闭连接 +// detailsResp, _, errors = detailsReq.End() +// } else { +// errors = append(errors, fmt.Errorf("请求失败,选择模式错误!")) +// } +// // 检查是否需要重试 +// shouldRetry := false +// if len(errors) > 0 { +// shouldRetry = true +// log.Printf("请求失败 (尝试 %d/%d): %v", attempt+1, maxRetries+1, errors) +// } else if detailsResp == nil { +// shouldRetry = true +// log.Printf("响应为空 (尝试 %d/%d)", attempt+1, maxRetries+1) +// } else if detailsResp.StatusCode != http.StatusOK { +// // 只对服务器错误进行重试,不对客户端错误重试 +// if detailsResp.StatusCode >= 500 { +// shouldRetry = true +// } +// log.Printf("HTTP状态码 %d,HTTP请求失败: %s (尝试 %d/%d)", detailsResp.StatusCode, detailsResp.Body, attempt+1, maxRetries+1) +// } +// // 如果不需要重试,跳出循环 +// if !shouldRetry { +// break +// } +// // 如果是最后一次尝试,不继续重试 +// if attempt == maxRetries { +// break +// } +// // 关闭响应体(如果存在) +// if detailsResp != nil && detailsResp.Body != nil { +// detailsResp.Body.Close() +// } +// } +// // 检测请求是否错误 +// if len(errors) > 0 { +// var proxyAuthFailed bool +// var timeoutError bool +// var connectionError bool +// for _, e := range errors { +// errStr := e.Error() +// if strings.Contains(errStr, "Proxy Authentication Required") { +// proxyAuthFailed = true +// } +// if strings.Contains(errStr, "timeout") || strings.Contains(errStr, "i/o timeout") { +// timeoutError = true +// } +// if strings.Contains(errStr, "connection") || strings.Contains(errStr, "connect") { +// 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) +// } +// if detailsResp.StatusCode != http.StatusOK { +// return nil, fmt.Errorf("HTTP错误: %s", detailsResp.Status) +// } +// +// detailsBody, err := io.ReadAll(detailsResp.Body) +// if err != nil { +// return nil, err +// } +// +// return goquery.NewDocumentFromReader(strings.NewReader(string(detailsBody))) +//} +// +//// 合并基本信息和详情 +//func mergeBookDetails(bookItems []BookItem, detailResults map[string]*DetailResult) []BookInfo { +// var books []BookInfo +// +// for _, item := range bookItems { +// book := item.Book +// +// // 如果有详情且获取成功,补充详细信息 +// if item.HasDetail && item.DetailURL != "" { +// if result, exists := detailResults[item.DetailURL]; exists && result.Error == nil && result.Doc != nil { +// detailsDiv := result.Doc.Find("div.detail-lists.clear-fix") +// if detailsDiv.Length() > 0 { +// selection := detailsDiv.Find("li") +// book.Author = extractNormalDetails(selection, "作者") +// book.Publisher = extractNormalDetails(selection, "出版社") +// detailsTime := extractNormalDetails(selection, "出版时间") +// if detailsTime != "" { +// formats := []string{"2006-01", "2006-01-02", "2006"} +// for _, format := range formats { +// if t, err := time.Parse(format, detailsTime); err == nil { +// book.PublicationTime = t.Unix() +// break +// } +// } +// } +// book.ISBN = extractNormalDetails(selection, "ISBN") +// book.Edition = extractNormalDetails(selection, "版次") +// book.FixPrice = extractNormalDetails(selection, "定价") +// book.BindingLayout = extractNormalDetails(selection, "装帧") +// book.Format = extractNormalDetails(selection, "开本") +// book.Pages = extractNormalDetails(selection, "页数") +// book.Wordage = extractNormalDetails(selection, "字数") +// } +// } +// } +// +// books = append(books, book) +// } +// return books +//} +// +//// 根据url获取单个图书详情信息 +//func getUrlBookDetails(fetchMode, proxyType, username, password, machineCode, url string) (books []BookInfo, err error) { +// document, err := fetchDocument(fetchMode, proxyType, username, password, machineCode, url) +// if err != nil { +// return nil, err +// } +// +// fee, err := FetchBookDetailsShippingFee(url) +// if err != nil { +// 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) { +// titleSpan := li.Find("span.keywords-define-title") +// contentSpan := li.Find("span.keywords-define-txt") +// if contentSpan.Length() == 0 { +// fmt.Printf("未找到指定的contentSpan信息") +// } +// titleText := strings.TrimSpace(titleSpan.Text()) +// contentText := strings.TrimSpace(contentSpan.Text()) +// titleText = strings.TrimSpace(titleText) +// if strings.Contains(titleText, "作者") { +// book.Author = cleanString(contentText) +// } +// if strings.Contains(titleText, "出版社") { +// book.Publisher = contentText +// } +// if strings.Contains(titleText, "出版人") { +// book.Publisher = contentText +// } +// if strings.Contains(titleText, "ISBN") { +// book.ISBN = contentText +// } +// if strings.Contains(titleText, "出版时间") { +// book.PublicationTime = validateDateFormat(contentText) +// } +// if strings.Contains(titleText, "版次") { +// book.Edition = contentText +// } +// if strings.Contains(titleText, "装帧") { +// book.BindingLayout = contentText +// } +// if strings.Contains(titleText, "开本") { +// book.Format = contentText +// } +// if strings.Contains(titleText, "页数") { +// book.Pages = contentText +// } +// if strings.Contains(titleText, "字数") { +// book.Wordage = contentText +// } +// if strings.Contains(titleText, "纸张") { +// book.Paper = contentText +// } +// if strings.Contains(titleText, "年代") { +// book.Era = contentText +// } +// if strings.Contains(titleText, "刻印方式") { +// book.EngravingMethod = contentText +// } +// if strings.Contains(titleText, "尺寸") { +// book.Dimensions = contentText +// } +// if strings.Contains(titleText, "册数") { +// book.VolumeNumber = contentText +// } +// }) +// } 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()) +// spanText = strings.TrimSpace(spanText) +// if strings.Contains(li.Text(), "作者") { +// book.Author = cleanString(li.Text()) +// book.Author = strings.ReplaceAll(book.Author, "作者:", "") +// book.Author = strings.ReplaceAll(book.Author, "著", "") +// } +// if strings.Contains(li.Text(), "出版社") { +// book.Publisher = spanText +// } +// if strings.Contains(li.Text(), "出版时间") { +// book.PublicationTime = validateDateFormat(spanText) +// } +// if strings.Contains(li.Text(), "ISBN") { +// book.ISBN = spanText +// } +// if strings.Contains(li.Text(), "装帧") { +// book.BindingLayout = spanText +// } +// if strings.Contains(li.Text(), "开本") { +// book.Format = spanText +// } +// if strings.Contains(li.Text(), "纸张") { +// book.Paper = spanText +// } +// if strings.Contains(li.Text(), "版次") { +// book.Edition = spanText +// } +// if strings.Contains(li.Text(), "页数") { +// book.Pages = spanText +// } +// if strings.Contains(li.Text(), "字数") { +// book.Wordage = spanText +// } +// }) +// } +// +// //图片 +// var imgUrls []string +// tpUl := document.Find("ul.lg-list") +// tpUl.Find("img").Each(func(i int, s *goquery.Selection) { +// dataImgUrl, exists := s.Attr("data-imgurl") +// if exists && dataImgUrl != "" { +// imgUrls = append(imgUrls, dataImgUrl) +// } +// }) +// 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) +// // 快递费 +// if fee != "" { +// if fee == "包邮" { +// book.ExpressDeliveryFee = "0" +// } else { +// book.ExpressDeliveryFee = fee +// } +// } +// books = append(books, book) +// return books, nil +//} +// +//// 获取店铺页面的所有快递费用 +//func FetchProductInfoWithChromedp(url string) (*ProductResponse, error) { +// // 创建上下文 +// opts := append(chromedp.DefaultExecAllocatorOptions[:], +// chromedp.Flag("headless", true), +// chromedp.Flag("disable-gpu", true), +// chromedp.Flag("no-sandbox", true), +// chromedp.Flag("disable-dev-shm-usage", true), +// chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"), +// // 禁用图片加载,加快速度 +// chromedp.Flag("blink-settings", "imagesEnabled=false"), +// ) +// // 启动浏览器 +// allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) +// defer cancel() +// // 创建浏览器上下文 +// ctx, cancel := chromedp.NewContext(allocCtx) +// defer cancel() +// // 设置超时 +// ctx, cancel = context.WithTimeout(ctx, 60*time.Second) +// defer cancel() +// log.Printf("[CHROMEDP] 开始导航到URL: %s", url) +// // 第一步:先导航到页面并等待完全加载 +// err := chromedp.Run(ctx, +// // 导航到目标页面 +// chromedp.Navigate(url), +// +// // 等待页面初步加载 +// chromedp.WaitReady("body", chromedp.ByQuery), +// chromedp.Sleep(3*time.Second), +// +// // 第二步:刷新页面,重新加载动态内容 +// chromedp.Reload(), +// +// // 等待刷新后页面加载 +// chromedp.WaitReady("body", chromedp.ByQuery), +// chromedp.Sleep(5*time.Second), +// +// // 等待关键元素出现 +// chromedp.WaitVisible(".item.clearfix", chromedp.ByQuery), +// +// // 滚动页面以触发动态加载 +// chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight / 3)`, nil), +// chromedp.Sleep(2*time.Second), +// chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight / 2)`, nil), +// chromedp.Sleep(2*time.Second), +// chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight)`, nil), +// chromedp.Sleep(3*time.Second), +// chromedp.Evaluate(`window.scrollTo(0, 0)`, nil), +// chromedp.Sleep(1*time.Second), +// ) +// if err != nil { +// log.Printf("[CHROMEDP] 页面导航失败: %v", err) +// return nil, fmt.Errorf("页面导航失败: %v", err) +// } +// log.Printf("[CHROMEDP] 页面导航完成,开始提取数据") +// var evalResult map[string]interface{} +// // 执行任务 +// err = chromedp.Run(ctx, +// // 导航到目标页面 +// chromedp.Navigate(url), +// +// // 执行 JavaScript 来提取商品信息 +// chromedp.Evaluate(`(function() { +// console.log("开始提取商品信息..."); +// try { +// let productData = []; +// // 查找所有商品项 - 针对孔夫子旧书网的特定选择器 +// const itemSelectors = [ +// '.item.clearfix', +// '.list-item', +// '.product-item', +// '.goods-item', +// '[class*="item"]' +// ]; +// let items = []; +// for (let selector of itemSelectors) { +// const found = document.querySelectorAll(selector); +// if (found.length > 0) { +// items = found; +// console.log("使用选择器:", selector, "找到项目数:", items.length); +// break; +// } +// } +// +// if (items.length === 0) { +// // 如果没有找到特定选择器,尝试查找任何看起来像商品的项目 +// items = document.querySelectorAll('div[class*="item"], li[class*="item"]'); +// console.log("使用通用选择器找到项目数:", items.length); +// } +// console.log("总共找到商品项:", items.length); +// // 遍历每个商品项 +// items.forEach((item, index) => { +// // 提取 itemid +// let itemId = ''; +// +// // 方法1: 从元素属性获取 +// if (item.hasAttribute('itemid')) { +// itemId = item.getAttribute('itemid'); +// } +// +// // 方法2: 从数据属性获取 +// if (!itemId && item.hasAttribute('data-itemid')) { +// itemId = item.getAttribute('data-itemid'); +// } +// +// // 方法3: 从ID属性获取 +// if (!itemId && item.hasAttribute('id')) { +// const id = item.getAttribute('id'); +// if (id.includes('item') || id.includes('product')) { +// itemId = id; +// } +// } +// +// // 方法4: 从链接中提取itemid +// if (!itemId) { +// const links = item.querySelectorAll('a[href*="item"], a[href*="product"]'); +// for (let link of links) { +// const href = link.getAttribute('href'); +// if (href) { +// const itemMatch = href.match(/(?:item|product)[_-]?(\d+)/i); +// if (itemMatch) { +// itemId = itemMatch[1]; +// break; +// } +// } +// } +// } +// +// // 方法5: 从子元素中查找itemid +// if (!itemId) { +// const childWithItemId = item.querySelector('[itemid], [data-itemid]'); +// if (childWithItemId) { +// if (childWithItemId.hasAttribute('itemid')) { +// itemId = childWithItemId.getAttribute('itemid'); +// } else if (childWithItemId.hasAttribute('data-itemid')) { +// itemId = childWithItemId.getAttribute('data-itemid'); +// } +// } +// } +// +// // 提取书名 +// let bookName = ''; +// const titleSelectors = [ +// '.title a', +// '.book-title', +// '.item-title', +// '.name a', +// 'h3 a', +// 'h4 a', +// '.link', +// 'a[title]' +// ]; +// +// for (let selector of titleSelectors) { +// const titleEl = item.querySelector(selector); +// if (titleEl) { +// bookName = titleEl.textContent.trim(); +// if (bookName && bookName.length > 1) { +// break; +// } +// } +// } +// +// // 如果没找到,尝试在item内查找任何文本作为书名 +// if (!bookName) { +// const textContent = item.textContent; +// // 尝试提取看起来像书名的文本(较长的文本块) +// const lines = textContent.split('\n').map(line => line.trim()).filter(line => line.length > 5); +// if (lines.length > 0) { +// bookName = lines[0]; +// } +// } +// +// // 提取价格 +// let price = ''; +// const priceSelectors = [ +// '.price span', +// '.price .bold', +// '.current-price', +// '.sell-price', +// '.cost', +// '.money', +// '[class*="price"]', +// 'strong' +// ]; +// +// for (let selector of priceSelectors) { +// const priceEl = item.querySelector(selector); +// if (priceEl) { +// let priceText = priceEl.textContent.trim(); +// // 清理价格文本,保留数字和小数点 +// priceText = priceText.replace(/[^\d\.]/g, ''); +// if (priceText && !isNaN(parseFloat(priceText))) { +// price = '¥' + priceText; +// break; +// } +// } +// } +// +// // 如果没找到价格,尝试在文本中查找价格模式 +// if (!price) { +// const itemText = item.textContent; +// const priceMatch = itemText.match(/[¥¥]?\s*(\d+\.?\d*)/); +// if (priceMatch) { +// price = '¥' + priceMatch[1]; +// } +// } +// +// // 提取快递费用 +// let shippingFee = ''; +// const shippingSelectors = [ +// '.ship-fee', +// '.shipping-fee', +// '.express-fee', +// '.freight', +// '.postage', +// '[class*="fee"]', +// '[class*="快递"]', +// '[class*="运费"]' +// ]; +// +// for (let selector of shippingSelectors) { +// const shippingEl = item.querySelector(selector); +// if (shippingEl) { +// shippingFee = shippingEl.textContent.trim(); +// if (shippingFee) break; +// } +// } +// +// // 如果没找到运费元素,尝试在文本中查找运费关键词 +// if (!shippingFee) { +// const itemText = item.textContent; +// const feeMatches = itemText.match(/(运费[::]\s*[^\s\n]+)|(快递[::]\s*[^\s\n]+)|(邮费[::]\s*[^\s\n]+)/); +// if (feeMatches) { +// shippingFee = feeMatches[0]; +// } else if (itemText.includes('包邮') || itemText.includes('免运费')) { +// shippingFee = '包邮'; +// } else { +// shippingFee = '运费待确认'; +// } +// } +// +// // 清理书名(移除过长的文本) +// if (bookName && bookName.length > 100) { +// bookName = bookName.substring(0, 100) + '...'; +// } +// +// // 如果还没有itemid,生成一个基于索引的ID +// if (!itemId) { +// itemId = 'item_' + (index + 1); +// } +// +// // 添加到数据中 +// productData.push({ +// itemId: itemId, +// bookName: bookName || '商品${index + 1}', +// price: price || '价格待确认', +// shippingFee: shippingFee +// }); +// }); +// +// // 过滤掉明显无效的数据 +// productData = productData.filter(item => +// (item.bookName !== '商品1' || item.price !== '价格待确认') && +// item.bookName && item.bookName.length > 0 +// ); +// +// console.log("处理后商品数量:", productData.length); +// if (productData.length > 0) { +// console.log("前3个商品示例:", productData.slice(0, 3)); +// } +// +// return { +// success: true, +// message: "成功提取商品信息", +// data: productData +// }; +// +// } catch(error) { +// console.error("提取商品信息时出错:", error); +// return { +// success: false, +// message: "提取商品信息时出错: " + error.message, +// data: [] +// }; +// } +// })()`, &evalResult), +// ) +// +// if err != nil { +// return nil, fmt.Errorf("chromedp执行失败: %v", err) +// } +// +// // 处理 JavaScript 执行结果 +// response := &ProductResponse{} +// +// // 转换结果数据 +// if success, ok := evalResult["success"].(bool); ok { +// response.Success = success +// } +// if message, ok := evalResult["message"].(string); ok { +// response.Message = message +// } +// if data, ok := evalResult["data"].([]interface{}); ok { +// for _, item := range data { +// if productMap, ok := item.(map[string]interface{}); ok { +// product := ProductInfo{} +// if itemId, ok := productMap["itemId"].(string); ok { +// product.ItemID = itemId +// } +// if bookName, ok := productMap["bookName"].(string); ok { +// product.BookName = bookName +// } +// if price, ok := productMap["price"].(string); ok { +// product.Price = price +// } +// if shippingFee, ok := productMap["shippingFee"].(string); ok { +// info := extractShippingInfo(shippingFee) +// product.ShippingFee = info +// } +// response.Data = append(response.Data, product) +// } +// } +// } +// +// return response, nil +//} +// +//// 获取详情页的快递费用 +//func FetchBookDetailsShippingFee(url string) (string, error) { +// // 创建上下文 +// opts := append(chromedp.DefaultExecAllocatorOptions[:], +// chromedp.Flag("headless", true), +// chromedp.Flag("disable-gpu", true), +// chromedp.Flag("no-sandbox", true), +// chromedp.Flag("disable-dev-shm-usage", true), +// chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"), +// // 禁用图片加载,加快速度 +// chromedp.Flag("blink-settings", "imagesEnabled=false"), +// ) +// // 启动浏览器 +// allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) +// defer cancel() +// // 创建浏览器上下文 +// ctx, cancel := chromedp.NewContext(allocCtx) +// defer cancel() +// // 设置超时 +// ctx, cancel = context.WithTimeout(ctx, 60*time.Second) +// defer cancel() +// log.Printf("[CHROMEDP] 开始导航到URL: %s", url) +// var result string +// +// // 增强的 JavaScript 脚本 +// enhancedJSScript := ` +// (function() { +// // 查找包含快递费用的元素 +// const elements = document.querySelectorAll('*'); +// let shippingFee = null; +// +// elements.forEach(element => { +// if (element.textContent.includes('快递¥')) { +// const match = element.textContent.match(/快递¥(\d+\.\d{2})/); +// if (match) { +// shippingFee = match[1]; +// console.log('找到快递费用: ¥' + shippingFee); +// } +// } +// // 匹配格式2: 快递:包邮 +// else if (element.textContent.includes('快递:包邮') || element.textContent.includes('快递:包邮')) { +// shippingFee = '包邮'; +// console.log('找到快递费用: 包邮'); +// } +// // 匹配格式3: 快递 包邮 (包含空格变体) +// else if (element.textContent.match(/快递[::\s]包邮/)) { +// shippingFee = '包邮'; +// console.log('找到快递费用: 包邮'); +// } +// }); +// +// if (!shippingFee) { +// // 如果没找到,尝试搜索整个页面 +// const pageText = document.body.innerText; +// const match = pageText.match(/快递¥(\d+\.\d{2})/); +// if (match) { +// shippingFee = match[1]; +// console.log('找到快递费用: ¥' + shippingFee); +// } +// // 尝试匹配包邮 +// else if (pageText.match(/快递[::\s]包邮/)) { +// shippingFee = '包邮'; +// console.log('找到快递费用: 包邮'); +// } +// // 尝试其他包邮表述 +// else if (pageText.includes('包邮') && pageText.includes('快递')) { +// shippingFee = '包邮'; +// console.log('找到快递费用: 包邮'); +// } +// else { +// console.log('未找到快递费用信息'); +// } +// } +// return shippingFee; +// })();` +// // 先导航到页面并等待完全加载 +// err := chromedp.Run(ctx, +// // 导航到目标页面 +// chromedp.Navigate(url), +// // 刷新页面,重新加载动态内容 +// chromedp.Reload(), +// // 等待刷新后页面加载 +// chromedp.WaitReady("body", chromedp.ByQuery), +// chromedp.Sleep(8*time.Second), +// // 执行 JavaScript 来提取商品信息 +// chromedp.Evaluate(enhancedJSScript, &result), +// ) +// if err != nil { +// log.Printf("[CHROMEDP] 页面导航失败: %v", err) +// return "", fmt.Errorf("页面导航失败: %v", err) +// } +// log.Printf("[CHROMEDP] 页面导航完成,开始提取数据") +// +// if err != nil { +// return "", fmt.Errorf("chromedp执行失败: %v", err) +// } +// return result, nil +//} +// +//// 获取孔网实拍图(查询商品列表) +//func getKFZSPTImageURL(proxyType, username, password, machineCode, isbn string) (*BookInfo, error) { +// url := fmt.Sprintf("%s?dataType=0&keyword=%s&page=1&size=%d&sortType=7&actionPath=quality,sortType&quality=85~&quaSelect=2&userArea=13003000000", cf.API.ProductSearchURL, isbn, cf.App.Size) +// +// req := XxProxyRequest(proxyType, username, password, machineCode). +// Get(url). +// Set("User-Agent", cf.App.DefaultUserAgent). +// Set("Accept", "*/*") +// resp, body, errs := req.End() +// log.Printf("[DEBUG] 获取图书列表 URL: %s", url) +// +// // 只在有响应内容时才打印,避免日志过长 +// if len(body) > 0 && len(body) < 500 { +// log.Printf("[DEBUG] 获取图书列表响应: %s", body) +// } else if len(body) > 0 { +// log.Printf("[DEBUG] 获取图书列表响应长度: %d 字符", len(body)) +// } else { +// log.Printf("[DEBUG] 获取图书列表响应为空") +// } +// +// // 处理请求错误 +// if len(errs) > 0 { +// // 检查是否是代理认证失败 +// var proxyAuthFailed bool +// for _, e := range errs { +// if strings.Contains(e.Error(), "Proxy Authentication Required") { +// proxyAuthFailed = true +// break +// } +// } +// if proxyAuthFailed { +// return nil, fmt.Errorf("代理认证失败") +// } +// return nil, fmt.Errorf("查询请求失败: %v", errs) +// } +// +// // 检查HTTP状态码 +// if resp.StatusCode != http.StatusOK { +// space := strings.TrimSpace(resp.Status) +// return nil, fmt.Errorf("HTTP错误: %s", space) +// } +// +// // 解析响应 +// var apiResp struct { +// Status int `json:"status"` +// ErrType string `json:"errType"` +// Message string `json:"message"` +// SystemTime int64 `json:"systemTime"` +// Data struct { +// ItemResponse struct { +// Total int `json:"total"` +// List []struct { +// Title string `json:"title"` +// ImgUrl string `json:"imgUrl"` +// ImgBigUrl string `json:"imgBigUrl"` +// ItemId int64 `json:"itemId"` +// ShopId int64 `json:"shopId"` +// TplRecords []struct { +// Key string `json:"key"` +// Value string `json:"value"` +// } `json:"tplRecords"` +// } `json:"list"` +// } `json:"itemResponse"` +// } `json:"data"` +// } +// +// if err := json.Unmarshal([]byte(body), &apiResp); err != nil { +// return nil, fmt.Errorf("解析JSON失败: %w", err) +// } +// +// // 如果找到商品,返回图片URL +// if apiResp.Data.ItemResponse.Total > 0 && len(apiResp.Data.ItemResponse.List) > 0 { +// // 确定起始索引 +// var startIndex int +// if cf.App.Size >= apiResp.Data.ItemResponse.Total { +// startIndex = apiResp.Data.ItemResponse.Total - 1 +// } else { +// startIndex = cf.App.Size - 1 +// } +// +// // 从指定索引开始,向前查找有效图片,最多重试3次 +// for attempt := 0; attempt < 3; attempt++ { +// currentIndex := startIndex - attempt +// +// // 检查索引是否有效 +// if currentIndex < 0 || currentIndex >= len(apiResp.Data.ItemResponse.List) { +// log.Printf("[DEBUG] 索引 %d 超出范围,跳过", currentIndex) +// continue +// } +// +// item := apiResp.Data.ItemResponse.List[currentIndex] +// +// // 检查图片URL是否存在 +// if item.ImgBigUrl == "" { +// log.Printf("[DEBUG] 索引 %d 的图片URL为空,跳过", currentIndex) +// continue +// } +// +// info := &BookInfo{} +// info.BookName = item.Title +// info.BookPicS = item.ImgUrl +// info.ItemId = item.ItemId +// info.ShopId = item.ShopId +// +// // 安全地获取TplRecords中的值 +// if len(item.TplRecords) > 0 { +// info.Author = item.TplRecords[0].Value +// } +// if len(item.TplRecords) > 1 { +// info.Publisher = item.TplRecords[1].Value +// } +// if len(item.TplRecords) > 2 { +// info.PublicationTime = validateDateFormat(item.TplRecords[2].Value) +// } +// if len(item.TplRecords) > 3 { +// info.BindingLayout = item.TplRecords[3].Value +// } +// +// // 成功时重置代理失败计数器 +// resetProxyFailCount() +// return info, nil +// } +// // 如果所有尝试都失败了,返回错误 +// log.Printf("[WARN] 经过3次尝试,未找到有效图片") +// return nil, fmt.Errorf("未找到有效图片,已尝试3次") +// } +// return nil, nil +//} +// +//// 获取孔网官图(查询图书条目) +//func getKFZGTImageURL(proxyType, username, password, machineCode, isbn string) (*BookInfo, error) { +// url := fmt.Sprintf("%s?keyword=%s", cf.API.BookSearchURL, isbn) +// +// info := &BookInfo{} +// +// req := XxProxyRequest(proxyType, username, password, machineCode). +// Get(url). +// Set("User-Agent", cf.App.DefaultUserAgent). +// Set("Accept", "*/*") +// resp, body, errs := req.End() +// log.Printf("[DEBUG] 获取图书条目 URL: %s", url) +// +// // 只在有响应内容时才打印,避免日志过长 +// if len(body) > 0 && len(body) < 500 { +// log.Printf("[DEBUG] 获取图书条目响应: %s", body) +// } else if len(body) > 0 { +// log.Printf("[DEBUG] 获取图书条目响应长度: %d 字符", len(body)) +// } else { +// log.Printf("[DEBUG] 获取图书条目响应为空") +// } +// +// // 处理请求错误 +// if len(errs) > 0 { +// // 检查是否是代理相关错误 +// var isProxyError bool +// var errorDetails []string +// for _, e := range errs { +// errorStr := e.Error() +// errorDetails = append(errorDetails, errorStr) +// if strings.Contains(errorStr, "Proxy Authentication Required") || +// strings.Contains(errorStr, "connectex: A connection attempt failed") || +// strings.Contains(errorStr, "connectex: No connection could be made") || +// strings.Contains(errorStr, "proxyconnect tcp") || +// strings.Contains(errorStr, "timeout") || +// strings.Contains(errorStr, "connection refused") { +// isProxyError = true +// } +// } +// +// log.Printf("[ERROR] 请求错误详情: %v", errorDetails) +// +// if isProxyError { +// // 处理代理失败 +// return nil, fmt.Errorf("代理连接失败") +// } +// +// return nil, fmt.Errorf("查询请求失败: %v", errs) +// } +// +// //检查HTTP状态码 +// if resp.StatusCode != http.StatusOK { +// space := strings.TrimSpace(resp.Status) +// return nil, fmt.Errorf("HTTP错误: %s", space) +// } +// +// // 解析响应 +// var apiResp struct { +// Status int `json:"status"` +// ErrType string `json:"errType"` +// Message string `json:"message"` +// SystemTime int64 `json:"systemTime"` +// Data struct { +// ItemResponse struct { +// Total int `json:"total"` +// List []struct { +// BookName string `json:"bookName"` +// Mid int64 `json:"mid"` +// ImgUrlEntity struct { +// BigImgUrl string `json:"bigImgUrl"` +// } `json:"imgUrlEntity"` +// BookShowInfo []string `json:"bookShowInfo"` +// } `json:"list"` +// } `json:"itemResponse"` +// } `json:"data"` +// } +// +// if err := json.Unmarshal([]byte(body), &apiResp); err != nil { +// return nil, fmt.Errorf("解析JSON失败: %w", err) +// } +// +// // 如果找到条目,返回图片URL +// if apiResp.Data.ItemResponse.Total > 0 && len(apiResp.Data.ItemResponse.List) > 0 { +// list := apiResp.Data.ItemResponse.List[0] +// info := list.BookShowInfo +// +// bookItem := &BookInfo{ +// BookName: list.BookName, +// BookPic: list.ImgUrlEntity.BigImgUrl, +// Mid: list.Mid, +// } +// +// // 根据长度安全填充字段 +// if len(info) > 0 { +// bookItem.Author = info[0] +// } +// if len(info) > 1 { +// bookItem.Publisher = info[1] +// } +// if len(info) > 2 { +// bookItem.PublicationTime = validateDateFormat(info[2]) +// } +// if len(info) > 3 { +// bookItem.BindingLayout = info[3] +// } +// if len(info) > 4 { +// bookItem.FixPrice = info[4] +// } else { +// log.Printf("[WARN] BookShowInfo 长度不足 (仅 %d 项): %v", len(info), info) +// } +// +// fmt.Println("bookItem: ", bookItem) +// return bookItem, nil +// } +// +// return info, nil +//} +// +//// 替换所有空白字符为空格 +//func cleanString(s string) string { +// s = strings.ReplaceAll(s, "\n", "") +// s = strings.ReplaceAll(s, "\r", "") +// s = strings.ReplaceAll(s, "\t", "") +// s = strings.ReplaceAll(s, " ", "") +// return removeDuplicates(s) +//} +// +//// 字符串去重 +//func removeDuplicates(s string) string { +// seen := make(map[rune]bool) +// var result strings.Builder +// for _, r := range s { +// if !seen[r] { +// seen[r] = true +// result.WriteRune(r) +// } +// } +// return result.String() +//} +// +//// 检测快递费用格式 +//func extractShippingInfo(shippingFee string) string { +// // 匹配 "快递¥数字" 格式 +// re1 := regexp.MustCompile(`快递¥(\d+\.?\d*)`) +// // 匹配 "包邮" 格式 +// re2 := regexp.MustCompile(`(包邮)`) +// +// if matches := re1.FindStringSubmatch(shippingFee); len(matches) > 1 { +// return matches[1] // 返回数字部分 +// } +// +// if matches := re2.FindStringSubmatch(shippingFee); len(matches) > 1 { +// return "0" // 返回"包邮" +// } +// +// return "" +//} +// +//// 改进字段 +//func extractNormalDetails(s *goquery.Selection, fieldName string) string { +// var info string +// s.Each(func(i int, selection *goquery.Selection) { +// if strings.Contains(selection.Text(), fieldName) { +// all := strings.ReplaceAll(selection.Text(), " ", "") +// all = strings.ReplaceAll(all, "\n", "") +// all = strings.ReplaceAll(all, " ", "") +// split := strings.SplitN(all, ":", 2) +// if len(split) > 1 { +// info = split[1] +// } else { +// return +// } +// } +// }) +// return info +//} +// +//// 改进的字段提取函数 +//func extractNormalText(s *goquery.Selection, fieldName string) string { +// // 在左右两个分区中查找 +// selectors := []string{"div.f_left", "div.f_right"} +// for _, selector := range selectors { +// fieldItem := s.Find(selector + " div.normal-item").FilterFunction(func(i int, sel *goquery.Selection) bool { +// title := sel.Find("span.normal-title").Text() +// return strings.Contains(title, fieldName) +// }) +// +// if fieldItem.Length() > 0 { +// text := fieldItem.Find("span.normal-text").Text() +// return strings.TrimSpace(text) +// } +// } +// return "" +//} +// +//// 重置代理失败计数器(成功时调用) +//func resetProxyFailCount() { +// tailProxyMu.Lock() +// defer tailProxyMu.Unlock() +// proxyFailCount = 0 +//} +// +//// 验证日期格式 +//func validateDateFormat(dateStr string) int64 { +// // 去除前后空格 +// dateStr = strings.TrimSpace(dateStr) +// +// // 替换各种分隔符为统一的分隔符"-" +// dateStr = regexp.MustCompile(`[/_\\.,\s]+`).ReplaceAllString(dateStr, "-") +// +// // 处理纯年份格式 (4位数字) +// if regexp.MustCompile(`^\d{4}$`).MatchString(dateStr) { +// dateStr += "-01-01" +// } +// +// // 处理年月格式 (4位数字-1或2位数字) +// if matches := regexp.MustCompile(`^(\d{4})-(\d{1,2})$`).FindStringSubmatch(dateStr); len(matches) == 3 { +// year := matches[1] +// month := matches[2] +// if len(month) == 1 { +// month = "0" + month +// } +// if monthNum, _ := strconv.Atoi(month); monthNum >= 1 && monthNum <= 12 { +// dateStr = year + "-" + month + "-01" +// } else { +// return 0 +// } +// } +// +// // 处理年月日格式 (4位数字-1或2位数字-1或2位数字) +// if matches := regexp.MustCompile(`^(\d{4})-(\d{1,2})-(\d{1,2})`).FindStringSubmatch(dateStr); len(matches) >= 4 { +// year := matches[1] +// month := matches[2] +// day := matches[3] +// +// // 标准化月份和日期为两位数 +// if len(month) == 1 { +// month = "0" + month +// } +// if len(day) == 1 { +// day = "0" + day +// } +// +// dateStr = year + "-" + month + "-" + day +// } +// +// // 加载上海时区 +// loc, err := time.LoadLocation("Asia/Shanghai") +// if err != nil { +// // 如果加载时区失败,使用UTC+8作为后备方案 +// loc = time.FixedZone("CST", 8*60*60) +// } +// +// // 尝试解析为标准日期格式,并指定上海时区 +// parsedTime, err := time.ParseInLocation("2006-01-02", dateStr, loc) +// if err != nil { +// return 0 +// } +// +// // 返回秒级时间戳(UTC时间,但解析时已经考虑了时区偏移) +// return parsedTime.Unix() +//} +// +//// 代理请求(小象代理) +//func XxProxyRequest(proxyType, username, password, machineCode string) *gorequest.SuperAgent { +// proxyManagerOnce.Do(func() { +// log.Printf("[INFO] 初始化代理管理器,DLL路径: %s", cf.Proxy.ProxyFilePath) +// +// // 检查DLL文件是否存在 +// if _, err := os.Stat(cf.Proxy.ProxyFilePath); os.IsNotExist(err) { +// // 尝试在可执行文件目录查找 +// exePath, _ := os.Executable() +// exeDir := filepath.Dir(exePath) +// dllPath := filepath.Join(exeDir, cf.Proxy.ProxyFilePath) +// +// if _, err := os.Stat(dllPath); err == nil { +// cf.Proxy.ProxyFilePath = dllPath +// } else { +// proxyManagerInitErr = fmt.Errorf("代理DLL文件不存在: %s (也尝试了: %s)", cf.Proxy.ProxyFilePath, dllPath) +// return +// } +// } +// +// globalProxyManager, proxyManagerInitErr = NewProxyConfigManager(cf.Proxy.ProxyFilePath) +// if proxyManagerInitErr != nil { +// log.Printf("[ERROR] 代理管理器初始化失败: %v", proxyManagerInitErr) +// } else { +// log.Printf("[INFO] 代理管理器初始化成功") +// } +// }) +// typeManager, err := globalProxyManager.ProxyTypeManager( +// proxyType, +// username, +// password, +// machineCode, +// ) +// if err != nil { +// fmt.Printf("获取代理服务器信息失败: %v", err) +// } +// return gorequest.New().Proxy(typeManager).Timeout(120*time.Second).Retry(2, 3*time.Second) +//} +// +//// 初始化 +//func initializeConfig(config Config) { +// // 设置全局配置 +// cf = config +//} +// +////export GetKFZShopBookInfo +//func GetKFZShopBookInfo(fetchMode, proxyType, username, password, machineCode, shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp *C.char) *C.char { +// fetchModeStr := C.GoString(fetchMode) +// proxyTypeStr := C.GoString(proxyType) +// usernameStr := C.GoString(username) +// passwordStr := C.GoString(password) +// machineCodeStr := C.GoString(machineCode) +// shopIdStr := C.GoString(shopId) +// isImageStr := C.GoString(isImage) +// bookNumStr := C.GoString(bookNum) +// pageNumStr := C.GoString(pageNum) +// sortTypeStr := C.GoString(sortType) +// sortStr := C.GoString(sort) +// priceDownStr := C.GoString(priceDown) +// priceUpStr := C.GoString(priceUp) +// +// books, goodsNum, pNum, err := getKFZShopBookInfo(fetchModeStr, proxyTypeStr, usernameStr, passwordStr, machineCodeStr, shopIdStr, isImageStr, bookNumStr, pageNumStr, sortTypeStr, sortStr, priceDownStr, priceUpStr) +// if err != nil { +// result := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// jsonData, _ := json.Marshal(result) +// return C.CString(string(jsonData)) +// } +// +// jsonData, err := json.Marshal(APIResponse{ +// Success: true, +// Message: "查询成功", +// GoodsNum: goodsNum, +// PNum: pNum, +// Data: books, +// }) +// if err != nil { +// errorResult := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// errorJson, _ := json.Marshal(errorResult) +// return C.CString(string(errorJson)) +// } +// +// return C.CString(string(jsonData)) +//} +// +////export GetUrlBookDetails +//func GetUrlBookDetails(fetchMode, proxyType, username, password, machineCode, url *C.char) *C.char { +// fetchModeStr := C.GoString(fetchMode) +// proxyTypeStr := C.GoString(proxyType) +// usernameStr := C.GoString(username) +// passwordStr := C.GoString(password) +// machineCodeStr := C.GoString(machineCode) +// urlStr := C.GoString(url) +// books, err := getUrlBookDetails(fetchModeStr, proxyTypeStr, usernameStr, passwordStr, machineCodeStr, urlStr) +// if err != nil { +// result := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// jsonData, _ := json.Marshal(result) +// return C.CString(string(jsonData)) +// } +// jsonData, err := json.Marshal(APIResponse{ +// Success: true, +// Message: "查询成功", +// Data: books, +// }) +// if err != nil { +// errorResult := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// errorJson, _ := json.Marshal(errorResult) +// return C.CString(string(errorJson)) +// } +// return C.CString(string(jsonData)) +//} +// +////export GetKFZSPTImageURL +//func GetKFZSPTImageURL(proxyType, username, password, machineCode, isbn *C.char) *C.char { +// proxyTypeStr := C.GoString(proxyType) +// usernameStr := C.GoString(username) +// passwordStr := C.GoString(password) +// machineCodeStr := C.GoString(machineCode) +// isbnStr := C.GoString(isbn) +// +// bookInfo, err := getKFZSPTImageURL(proxyTypeStr, usernameStr, passwordStr, machineCodeStr, isbnStr) +// if err != nil { +// result := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// jsonData, _ := json.Marshal(result) +// return C.CString(string(jsonData)) +// } +// +// result := map[string]interface{}{ +// "success": true, +// "bookInfo": bookInfo, +// } +// jsonData, err := json.Marshal(result) +// if err != nil { +// errorResult := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// errorJson, _ := json.Marshal(errorResult) +// return C.CString(string(errorJson)) +// } +// return C.CString(string(jsonData)) +//} +// +////export GetKFZGTImageURL +//func GetKFZGTImageURL(proxyType, username, password, machineCode, isbn *C.char) *C.char { +// proxyTypeStr := C.GoString(proxyType) +// usernameStr := C.GoString(username) +// passwordStr := C.GoString(password) +// machineCodeStr := C.GoString(machineCode) +// isbnStr := C.GoString(isbn) +// +// bookInfo, err := getKFZGTImageURL(proxyTypeStr, usernameStr, passwordStr, machineCodeStr, isbnStr) +// if err != nil { +// result := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// jsonData, _ := json.Marshal(result) +// return C.CString(string(jsonData)) +// } +// +// result := map[string]interface{}{ +// "success": true, +// "bookInfo": bookInfo, +// } +// jsonData, err := json.Marshal(result) +// if err != nil { +// errorResult := map[string]interface{}{ +// "success": false, +// "error": err.Error(), +// } +// errorJson, _ := json.Marshal(errorResult) +// return C.CString(string(errorJson)) +// } +// +// return C.CString(string(jsonData)) +//} +// +////export Initialize +//func Initialize(configJSON *C.char) *C.char { +// configStr := C.GoString(configJSON) +// log.Printf("[DEBUG] 接收到的配置JSON: %s", configStr) +// +// var config Config +// if err := json.Unmarshal([]byte(configStr), &config); err != nil { +// return C.CString(fmt.Sprintf(`{"success":false,"message":"配置解析失败: %v"}`, err)) +// } +// initializeConfig(config) +// +// return C.CString(`{"success":true,"message":"初始化成功"}`) +//} +// +//// 导出函数:释放C字符串内存 +//// +////export FreeCString +//func FreeCString(str *C.char) { +// C.free(unsafe.Pointer(str)) +//} +// +//// 空main函数,编译DLL时需要 +//func main() { +// +//} diff --git a/main.go b/main.go new file mode 100644 index 0000000..5109e33 --- /dev/null +++ b/main.go @@ -0,0 +1,2997 @@ +package main + +/* + #include + + // proxyConfig.dll 函数声明 + extern char* ProxyTypeManager(char* proxyType, char* username, char* password, char* machineCode); + extern void FreeCString(char* str); +*/ +import "C" +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/PuerkitoBio/goquery" + "github.com/chromedp/chromedp" + _ "github.com/go-sql-driver/mysql" + "github.com/parnurzeal/gorequest" +) + +const ( + UseProxy = "proxy" // 使用代理 + NotProxy = "direct" // 不用代理代理 +) + +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"` + } `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"` + } `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"` + } `ini:"proxy" json:"proxy"` + + Database struct { + Username string `ini:"database.username" json:"username" default:"newAdmin"` + Password string `ini:"database.password" json:"password" default:"bYPp8SbBe5F7nz2i"` + Host string `ini:"database.host" json:"host" default:"146.56.227.42:3306"` + Name string `ini:"database.name" json:"name" default:"newadmin"` + } `ini:"database" json:"database"` +} + +// 条目详情结构体 +type BookInfo struct { + BookName string `json:"book_name"` // 书名 + Author string `json:"author"` // 作者 + Publisher string `json:"publisher"` // 出版社 + ISBN string `json:"isbn"` // ISBN + PublicationTime int64 `json:"publication_time"` // 出版时间 + Edition string `json:"edition"` // 版次 + PrintTime string `json:"print_time"` // 印刷时间 + FixPrice string `json:"fix_price"` // 定价 + BindingLayout string `json:"binding_layout"` // 装帧 + Format string `json:"format"` // 开本 + Paper string `json:"paper"` // 纸张 + Pages string `json:"pages"` // 页数 + Wordage string `json:"wordage"` // 字数 + Languages string `json:"languages"` // 语种 + Era string `json:"era"` // 年代 + EngravingMethod string `json:"engraving_method"` // 刻印方式 + Dimensions string `json:"dimensions"` // 尺寸 + VolumeNumber string `json:"volume_number"` // 册数 + BookPic string `json:"book_pic"` // 图书封面图(官图) + BookPicS string `json:"book_pic_s"` // 图书封面图(实拍图) + SellingPrice string `json:"selling_price"` // 售价 + Condition string `json:"condition"` // 品相 + ExpressDeliveryFee string `json:"express_delivery_fee"` // 快递费 + Editor string `json:"editor"` // 编辑 + Category string `json:"category"` // 分类 + BuyCount string `json:"buy_count"` // 买过 + SellCount string `json:"sell_count"` // 在卖 + Content string `json:"content"` // 内容 + Mid int64 `json:"mid"` // 商家id + ItemId int64 `json:"item_id"` // 商品id + ShopId int64 `json:"shop_id"` // 店铺id + DetailUrl string `json:"detail_url"` // 商品详情url +} + +// API响应结构 +type APIResp struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + GoodsNum string `json:"goods_num,omitempty"` + PNum string `json:"pnum,omitempty"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +// APIResponse +type APIResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +// 账号凭证结构 +type AccountCredential struct { + ID int64 + Username string + Password string + Token string +} + +// 全局变量 +var ( + cf Config // 配置信息 + tailProxyMu sync.Mutex // 互斥锁 + proxyFailCount int + mu sync.RWMutex // 数据库锁 + + // 全局代理管理器 + globalProxyManager *ProxyConfigManager + proxyManagerOnce sync.Once + proxyManagerInitErr error +) + +// ProductInfo 商品信息结构 +type ProductInfo struct { + ItemID string `json:"itemId"` + BookName string `json:"bookName"` + Price string `json:"price"` + ShippingFee string `json:"shippingFee"` +} + +// ProductResponse 响应结构 +type ProductResponse struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Data []ProductInfo `json:"data,omitempty"` +} + +// 并行获取详情的结果结构 +type DetailResult struct { + URL string + Doc *goquery.Document + Error error + Index int +} + +// 第一阶段:收集基本信息并识别需要详情的项目 +type BookItem struct { + Book BookInfo + Selection *goquery.Selection + HasDetail bool + DetailURL string + Index int +} + +// 分类项结构体 +type SalesCategory struct { + Key string `json:"key"` + Value int `json:"value"` +} + +// 图书详情响应结构体 +type BookDetailResponse struct { + Status bool `json:"status"` + Result BookList `json:"result"` + ErrMessage string `json:"errMessage"` + ErrCode int `json:"errCode"` +} + +// 图书列表结构体 +type BookList struct { + Current int `json:"current"` + Data []BookInformation `json:"data"` + Total int `json:"total"` +} + +// 图书信息结构体 +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"` +} + +// 作者信息结构体 +type AuthorInfo struct { + Name string `json:"name"` + OriName string `json:"oriName"` + Nationality string `json:"nationality"` + Role string `json:"role"` + Url string `json:"url"` +} + +// 商品链接结构体 +type ItemUrls struct { + AppUrl string `json:"appUrl"` + MUrl string `json:"mUrl"` + MiniUrl string `json:"miniUrl"` + PcUrl string `json:"pcUrl"` +} + +type UserInfo struct { + UserID int64 `json:"userId"` + Nickname string `json:"nickname"` + Mobile string `json:"mobile"` +} + +// ProxyConfigManager 代理配置DLL管理器 +type ProxyConfigManager struct { + dll *syscall.DLL +} + +func NewProxyConfigManager(dllPath string) (*ProxyConfigManager, error) { + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载代理配置DLL失败: %v", err) + } + return &ProxyConfigManager{dll: dll}, nil +} + +func (m *ProxyConfigManager) Close() { + if m.dll != nil { + m.dll.Release() + } +} + +// ProxyTypeManager 调用代理类型管理器 +func (m *ProxyConfigManager) ProxyTypeManager(proxyType, username, password, machineCode string) (string, error) { + proc, err := m.dll.FindProc("ProxyTypeManager") + if err != nil { + return "", fmt.Errorf("找不到函数 ProxyTypeManager: %v", err) + } + // 准备参数 + proxyTypePtr, _ := syscall.BytePtrFromString(proxyType) + usernamePtr, _ := syscall.BytePtrFromString(username) + passwordPtr, _ := syscall.BytePtrFromString(password) + machineCodePtr, _ := syscall.BytePtrFromString(machineCode) + // 调用函数 + r1, _, err := proc.Call( + uintptr(unsafe.Pointer(proxyTypePtr)), + uintptr(unsafe.Pointer(usernamePtr)), + uintptr(unsafe.Pointer(passwordPtr)), + uintptr(unsafe.Pointer(machineCodePtr)), + ) + + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用 ProxyTypeManager 失败: %v", err) + } + // 转换结果 + result := (*byte)(unsafe.Pointer(r1)) + var resultBytes []byte + for i := 0; ; i++ { + bytePtr := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) + if *bytePtr == 0 { + break + } + resultBytes = append(resultBytes, *bytePtr) + } + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeCString") + if freeProc != nil { + freeProc.Call(r1) + } + return string(resultBytes), nil +} + +// 登录 +func outLogin(username, password string) (string, error) { + if username == "" || password == "" { + return "", fmt.Errorf("请输入用户名和密码!") + } + formData := map[string]string{ + "loginName": username, + "loginPass": password, + "returnUrl": "http://user.kongfz.com/", + } + resp, body, errs := gorequest.New(). + Post(cf.API.LoginURL). + Set("Content-Type", "application/x-www-form-urlencoded"). + Set("User-Agent", cf.App.DefaultUserAgent). + Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"). + Send(formData). + 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) + } + // 提取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") + } + // 登录成功 + if strings.Contains(cookie, "PHPSESSID=") { + token := strings.Split(strings.Split(cookie, "PHPSESSID=")[1], ";")[0] + return token, nil + } + return "", fmt.Errorf("登录失败: 未找到PHPSESSID") + } + // 错误信息 + var res struct { + Status bool `json:"status"` + ErrCode int `json:"errCode"` + ErrInfo string `json:"errInfo"` + } + if err := json.Unmarshal([]byte(body), &res); err == nil { + if res.ErrCode == 1001 || res.ErrCode == 1005 { + return "", fmt.Errorf("账号或密码错误!") + } + if res.ErrInfo != "" { + return "", fmt.Errorf("登录失败: %s", res.ErrInfo) + } + } + return "", fmt.Errorf("登录失败,未知错误!") +} + +// 获取用户信息(带有Out的都非官方标准接口) +func outGetUserMsg(token string) (*UserInfo, error) { + url := "https://user.kongfz.com/User/Index/getUserInfo/" + resp, body, errs := gorequest.New(). + Get(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"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(15 * time.Second). + End() + if len(errs) > 0 { + return nil, fmt.Errorf("查询请求失败: %v", errs) + } + //检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", resp.Status) + } + var userInfo struct { + Status bool `json:"status"` + Data struct { + UserID int64 `json:"userId"` + Nickname string `json:"nickname"` + Mobile string `json:"mobile"` + } + } + if err := json.Unmarshal([]byte(body), &userInfo); err != nil { + return nil, fmt.Errorf("解析JSON失败: %w", err) + } + user := &UserInfo{} + if !userInfo.Status { + return nil, fmt.Errorf("获取用户失败!") + } + user.UserID = userInfo.Data.UserID + user.Nickname = userInfo.Data.Nickname + user.Mobile = userInfo.Data.Mobile + return user, nil +} + +// 获取商品模版 +func outGetGoodsTplMsg(token, itemId, proxy string) (map[string]interface{}, error) { + if token == "" { + return nil, fmt.Errorf("请先登录获取Token") + } + url := fmt.Sprintf("https://seller.kongfz.com/pc/itemInfo/getTplFields?itemId=%s&isClone=1&v=%d", + itemId, time.Now().Unix()) + // 创建HTTP客户端 + request := gorequest.New() + // 设置代理(如果有提供代理URL) + if proxy != "" { + request.Proxy(proxy) + } + resp, body, errs := request.Get(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"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Set("Referer", "https://seller.kongfz.com/"). + Set("Origin", "https://seller.kongfz.com"). + Timeout(15 * time.Second). + End() + if errs != nil { + // 检查是否是代理相关错误 + var isProxyError bool + var errorDetails []string + for _, e := range errs { + errorStr := e.Error() + errorDetails = append(errorDetails, errorStr) + if strings.Contains(errorStr, "Proxy Authentication Required") || + strings.Contains(errorStr, "connectex: A connection attempt failed") || + strings.Contains(errorStr, "connectex: No connection could be made") || + strings.Contains(errorStr, "proxyconnect tcp") || + strings.Contains(errorStr, "timeout") || + strings.Contains(errorStr, "connection refused") { + isProxyError = true + } + } + log.Printf("[ERROR] 请求错误详情: %v", errorDetails) + if isProxyError { + // 处理代理失败 + return nil, fmt.Errorf("代理连接失败") + } + return nil, fmt.Errorf("查询请求失败: %v", errs) + } + //检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", resp.Status) + } + 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"].(float64); ok && val == 1 { + return data, nil + } + return nil, fmt.Errorf("API返回错误: %+v", data) +} + +// 获取商品列表 +func outGetGoodsListMsgFromSelfShop(token string, proxy string, itemSn string, priceMin string, priceMax string, startCreateTime int, + endCreateTime int, requestType string, isItemSnEqual int, page int, size int) (map[string]interface{}, error) { + if token == "" { + return nil, fmt.Errorf("请先登录获取Token") + } + url := "https://seller.kongfz.com/pc-gw/book-manage-service/client/pc/goods/unSold/list" + formData := struct { + ItemSn string `json:"itemSn"` + PriceMin string `json:"priceMin"` + PriceMax string `json:"priceMax"` + StartCreateTime int `json:"startCreateTime"` + EndCreateTime int `json:"endCreateTime"` + RequestType string `json:"requestType,omitempty"` + IsItemSnEqual int `json:"isItemSnEqual,omitempty"` + Page int `json:"page,omitempty"` + Size int `json:"size,omitempty"` + }{ + ItemSn: itemSn, + PriceMin: priceMin, + PriceMax: priceMax, + StartCreateTime: startCreateTime, + EndCreateTime: endCreateTime, + RequestType: requestType, + IsItemSnEqual: isItemSnEqual, + Page: page, + Size: size, + } + request := gorequest.New() + if proxy != "" { + request.Proxy(proxy) + } + 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"). + Set("Accept", "application/json, text/plain, */*"). + Send(formData). + Timeout(15 * time.Second). + End() + if errs != nil { + // 检查是否是代理相关错误 + var isProxyError bool + var errorDetails []string + for _, e := range errs { + errorStr := e.Error() + errorDetails = append(errorDetails, errorStr) + if strings.Contains(errorStr, "Proxy Authentication Required") || + strings.Contains(errorStr, "connectex: A connection attempt failed") || + strings.Contains(errorStr, "connectex: No connection could be made") || + strings.Contains(errorStr, "proxyconnect tcp") || + strings.Contains(errorStr, "timeout") || + strings.Contains(errorStr, "connection refused") { + isProxyError = true + } + } + log.Printf("[ERROR] 请求错误详情: %v", errorDetails) + if isProxyError { + // 处理代理失败 + return nil, fmt.Errorf("代理连接失败") + } + return nil, fmt.Errorf("查询请求失败: %v", errs) + } + //检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", resp.Status) + } + 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) +} + +// 新增商品(带有Out的都非官方标准接口) +func outAddGoods(token, proxy, formData string) (map[string]interface{}, error) { + url := "https://seller.kongfz.com/pc/itemInfo/add" + request := gorequest.New() + if proxy != "" { + request.Proxy(proxy) + } + 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"). + Set("Accept", "application/json, text/plain, */*"). + Send(formData). + Timeout(15 * time.Second). + End() + if errs != nil { + // 检查是否是代理相关错误 + var isProxyError bool + var errorDetails []string + for _, e := range errs { + errorStr := e.Error() + errorDetails = append(errorDetails, errorStr) + if strings.Contains(errorStr, "Proxy Authentication Required") || + strings.Contains(errorStr, "connectex: A connection attempt failed") || + strings.Contains(errorStr, "connectex: No connection could be made") || + strings.Contains(errorStr, "proxyconnect tcp") || + strings.Contains(errorStr, "timeout") || + strings.Contains(errorStr, "connection refused") { + isProxyError = true + } + } + log.Printf("[ERROR] 请求错误详情: %v", errorDetails) + if isProxyError { + // 处理代理失败 + return nil, fmt.Errorf("代理连接失败") + } + return nil, fmt.Errorf("查询请求失败: %v", errs) + } + //检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", resp.Status) + } + 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) +} + +// 获取图片URL(官图和拍图) +func outGetImageByIsbn(token string, isbn string, proxy string, isLiveImage int, isReturnMsg int) (*BookInfo, error) { + fmt.Println("[DEBUG] 使用的ISBN: ", isbn) + // isLiveImage 1实拍图 0官图 ,isReturnMsg 0商品信息 + bookInfo := &BookInfo{} + if isLiveImage == 0 { + // 孔网官图请求 + gtUrl := fmt.Sprintf("%s?keyword=%s", cf.API.BookSearchURL, isbn) + // 创建HTTP客户端 + requestGt := gorequest.New() + // 设置代理(如果有提供代理URL) + if proxy != "" { + requestGt.Proxy(proxy) + } + // 发送请求 + respGt, bodyGt, errsGt := requestGt.Get(gtUrl). + Set("Cookie", 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"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Set("Referer", "https://item.kongfz.com/"). + Timeout(30 * time.Second). + End() + if len(errsGt) > 0 { + // 检查是否是代理相关错误 + var isProxyError bool + var errorDetails []string + for _, e := range errsGt { + errorStr := e.Error() + errorDetails = append(errorDetails, errorStr) + if strings.Contains(errorStr, "Proxy Authentication Required") || + strings.Contains(errorStr, "connectex: A connection attempt failed") || + strings.Contains(errorStr, "connectex: No connection could be made") || + strings.Contains(errorStr, "proxyconnect tcp") || + strings.Contains(errorStr, "timeout") || + strings.Contains(errorStr, "connection refused") { + isProxyError = true + } + } + log.Printf("[ERROR] 请求错误详情: %v", errorDetails) + if isProxyError { + // 处理代理失败 + return nil, fmt.Errorf("代理连接失败") + } + return nil, fmt.Errorf("查询请求失败: %v", errsGt) + } + //检查HTTP状态码 + if respGt.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", respGt.Status) + } + // 解析响应 + var apiGtResp struct { + Status int `json:"status"` + ErrType string `json:"errType"` + Message string `json:"message"` + SystemTime int64 `json:"systemTime"` + Data struct { + ItemResponse struct { + Total int `json:"total"` + List []struct { + BookName string `json:"bookName"` + Mid int64 `json:"mid"` + ImgUrlEntity struct { + BigImgUrl string `json:"bigImgUrl"` + } `json:"imgUrlEntity"` + Isbn string `json:"isbn"` + BookShowInfo []string `json:"bookShowInfo"` + } `json:"list"` + } `json:"itemResponse"` + } `json:"data"` + } + if err := json.Unmarshal([]byte(bodyGt), &apiGtResp); err != nil { + return nil, fmt.Errorf("解析JSON失败: %w", err) + } + if apiGtResp.ErrType == "102" { + return nil, fmt.Errorf("错误信息: %w,状态码: %s", apiGtResp.Message, apiGtResp.ErrType) + } + // 如果找到条目,返回图片URL + if apiGtResp.Data.ItemResponse.Total > 0 && len(apiGtResp.Data.ItemResponse.List) > 0 { + list := apiGtResp.Data.ItemResponse.List[0] + bookShowInfo := list.BookShowInfo + bookInfo.BookName = list.BookName + bookInfo.BookPic = list.ImgUrlEntity.BigImgUrl + bookInfo.ISBN = list.Isbn + // 根据长度安全填充字段 + if isReturnMsg == 0 { + if len(bookShowInfo) > 0 { + bookInfo.Author = bookShowInfo[0] + } + if len(bookShowInfo) > 1 { + bookInfo.Publisher = bookShowInfo[1] + } + if len(bookShowInfo) > 2 { + bookInfo.PublicationTime = validateDateFormat(bookShowInfo[2]) + } + if len(bookShowInfo) > 3 { + bookInfo.BindingLayout = bookShowInfo[3] + } + if len(bookShowInfo) > 4 { + bookInfo.FixPrice = bookShowInfo[4] + } else { + log.Printf("[WARN] BookShowInfo 长度不足 (仅 %d 项): %v", len(bookShowInfo), bookShowInfo) + } + } + } + return bookInfo, nil + } + if isLiveImage == 1 { + // 实拍图 + sptUrl := fmt.Sprintf("%s?dataType=0&keyword=%s&page=1&size=%d&sortType=7&actionPath=quality,sortType&quality=85~&quaSelect=2&userArea=13003000000", cf.API.ProductSearchURL, isbn, cf.App.Size) + //创建HTTP客户端 + requestSpt := gorequest.New() + //设置代理(如果有提供代理URL) + if proxy != "" { + requestSpt.Proxy(proxy) + } + // 发送请求 + respSpt, bodySpt, errsSpt := requestSpt.Get(sptUrl). + Proxy(proxy). + Set("Cookie", 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"). + Set("Accept", "application/json, text/plain, */*"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Set("Referer", "https://item.kongfz.com/"). + Timeout(30 * time.Second). + End() + // 错误处理 + if len(errsSpt) > 0 { + return nil, fmt.Errorf("请求失败: %v", errsSpt) + } + // 检查HTTP状态码 + if respSpt.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", respSpt.Status) + } + // 解析响应 + var apiSptResp struct { + Status int `json:"status"` + ErrType string `json:"errType"` + Message string `json:"message"` + SystemTime int64 `json:"systemTime"` + Data struct { + ItemResponse struct { + Total int `json:"total"` + List []struct { + ItemId int64 `json:"itemId"` + Title string `json:"title"` + ImgUrl string `json:"imgUrl"` + ImgBigUrl string `json:"imgBigUrl"` + Author string `json:"author"` + PubDateText string `json:"pubDateText"` + Isbn string `json:"isbn"` + Press string `json:"press"` + ShopId int64 `json:"shopId"` + TplRecords []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"tplRecords"` + } `json:"list"` + } `json:"itemResponse"` + } `json:"data"` + } + // 解析JSON + if err := json.Unmarshal([]byte(bodySpt), &apiSptResp); err != nil { + return nil, fmt.Errorf("解析JSON失败: %w", err) + } + if apiSptResp.ErrType == "102" { + return nil, fmt.Errorf("错误信息: %w,状态码: %s", apiSptResp.Message, apiSptResp.ErrType) + } + if apiSptResp.Data.ItemResponse.Total > 0 && len(apiSptResp.Data.ItemResponse.List) > 0 { + // 确定其实索引 + var startIndex int + if cf.App.Size >= apiSptResp.Data.ItemResponse.Total { + startIndex = apiSptResp.Data.ItemResponse.Total - 1 + } else { + startIndex = cf.App.Size - 1 + } + for attempt := 0; attempt < 3; attempt++ { + currentIndex := startIndex - attempt + // 检查索引是否有效 + if currentIndex < 0 || currentIndex >= len(apiSptResp.Data.ItemResponse.List) { + log.Printf("[DEBUG] 索引 %d 超出范围,跳过", currentIndex) + continue + } + randomNum := rand.Intn(currentIndex + 1) + item := apiSptResp.Data.ItemResponse.List[randomNum] + // 检查图片URL是否存在 + if item.ImgBigUrl == "" { + 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 { + // 安全地获取TplRecords中的值 + if len(item.TplRecords) > 0 { + bookInfo.Author = item.TplRecords[0].Value + } + if len(item.TplRecords) > 1 { + bookInfo.Publisher = item.TplRecords[1].Value + } + if len(item.TplRecords) > 2 { + bookInfo.PublicationTime = validateDateFormat(item.TplRecords[2].Value) + } + if len(item.TplRecords) > 3 { + bookInfo.BindingLayout = item.TplRecords[3].Value + } + } + } + return bookInfo, nil + } + } + } + return nil, fmt.Errorf("查询失败,没有数据!") +} + +// 登录,获取账号cookie +func loginCookie() (cookie string, err error) { + account := &AccountCredential{ + ID: 0, + Username: "", + Password: "", + Token: "", + } + var change = false + if account.Username == "" || change { + account, err = getRandomAccount() + if err != nil { + log.Printf("获取账号失败: %v", err) + } + change = false + } + fmt.Println("获取的账号: ", account.Username) + // 将拿出的账号token赋值给cookie + cookie = account.Token + // 若cookie为""说明该账号没有token还未登录过,进行登录 + if cookie == "" { + // 登录获取cookie + cookie, err = loginAndGetCookie(account) + if err != nil { + // 若登录失败,则标记为需要更换账号,然后重试 + change = true + log.Printf("登录获取cookie失败: %v ,更换账号重试", err) + } + // 成功拿到cookie,则将新拿到的cookie赋值给account的token + account.Token = cookie + // 将新拿到的cookie同步更新token到数据库 + err = updateAccountToken(account.ID, cookie) + if err != nil { + log.Printf("更新账号token失败: %v", err) + } + } + return cookie, nil +} + +// 登录并获取cookie +func loginAndGetCookie(account *AccountCredential) (string, error) { + formData := map[string]string{ + "loginName": account.Username, + "loginPass": account.Password, + } + // 没有代理 + resp, body, errs := gorequest.New(). + Post(cf.API.LoginURL). + Set("Content-Type", "application/x-www-form-urlencoded"). + Set("User-Agent", cf.App.DefaultUserAgent). + Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"). + Send(formData). + Timeout(30 * time.Second). + End() + // 处理请求错误 + if len(errs) > 0 { + // 检查是否是代理认证失败 + var proxyAuthFailed bool + for _, e := range errs { + if strings.Contains(e.Error(), "Proxy Authentication Required") { + proxyAuthFailed = true + break + } + } + if proxyAuthFailed { + return "", fmt.Errorf("代理认证失败") + } + return "", fmt.Errorf("登录请求失败: %v", errs) + } + // 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("登录失败(HTTP状态码: %d)", resp.StatusCode) + } + if !strings.Contains(body, "window.location.href='https://login.kongfz.cn/Pc/Session/rsync") { + // 尝试解析JSON响应获取错误信息 + var response struct { + Status bool `json:"status"` + ErrCode *int `json:"errCode"` + ErrInfo string `json:"errInfo"` + } + if err := json.Unmarshal([]byte(body), &response); err != nil { + // 账号密码错误 + if response.ErrCode != nil && *response.ErrCode == 1001 { + // 标记账号为不可用 + if err := updateAccountInvalid(account.ID); err != nil { + log.Printf("标记账号不可用失败: %v", err) + } + return "", fmt.Errorf("账号或密码错误") + } + if response.ErrInfo != "" { + if err := updateAccountInvalid(account.ID); err != nil { + log.Printf("标记账号不可用失败: %v", err) + } + return "", fmt.Errorf(response.ErrInfo) + } else { + if err := updateAccountNeedVerify(account.ID); err != nil { + log.Printf("标记账号需要验证失败: %v", err) + } + return "", fmt.Errorf("该账号需要手机号登录验证") + } + } else { + return "", fmt.Errorf("登录响应不包含成功跳转信息") + } + } + // 提取Cookie + cookies := resp.Header.Get("Set-Cookie") + if cookies == "" { + return "", fmt.Errorf("登录成功但未获取到Cookie") + } + log.Printf("登录成功,用户: %s", account.Username) + return cookies, nil +} + +// 获取商品列表通过店铺ID +func outGetGoodsListMsgByShopId(shopId int, proxy string, isImage int, sortType string, sort string, priceMin float32, priceMax float32, pageNum, returnNum int) (books []BookInfo, goodsNum string, pNum string, err error) { + // 判断店铺ID + if shopId == 0 { + return nil, "", "", fmt.Errorf("店铺编码为空!") + } + // 判断是否有图片,设置默认值0 + var isImageStr string + if isImage == 0 { + isImageStr = "0" + } else { + isImageStr = "1" + } + // 判断一页图书数量,设置默认值100 + if returnNum == 0 { + returnNum = 100 + } + // 判断页数,设置默认值1 + if pageNum == 0 { + pageNum = 1 + } + // 判断排序类型,设置默认值sort + if sortType == "" { + sortType = "sort" + } else { + validSortTypes := map[string]bool{ + "sort": true, + "putDate": true, + "newItem": true, + "price": true, + } + if !validSortTypes[sortType] { + return nil, "", "", fmt.Errorf("无效的排序类型: %s,可选值: sort, putDate, newItem, price", sortType) + } + } + // 判断排序,设置默认值desc + if sort == "" { + sort = "desc" + } else { + validSorts := map[string]bool{ + "desc": true, + "asc": true, + } + if !validSorts[sort] { + return nil, "", "", fmt.Errorf("无效的排序类型: %s,可选值: desc, asc", sort) + } + } + var url string + var pMin int + pMin = 0 + var pMax int + pMax = 0 + // 判断价格下限,设置默认值0 + if priceMin == 0 && priceMax == 0 { + // 调用的url + url = fmt.Sprintf("https://shop.kongfz.com/%d/all/%s_%d_0_0_%d_%s_%s_%d_%d", + shopId, isImageStr, returnNum, pageNum, sortType, sort, pMin, pMax) + } else if priceMin == 0 { + url = fmt.Sprintf("https://shop.kongfz.com/%d/all/%s_%d_0_0_%d_%s_%s_%d_%.2f", + shopId, isImageStr, returnNum, pageNum, sortType, sort, pMin, priceMax) + } else if priceMax == 0 { + url = fmt.Sprintf("https://shop.kongfz.com/%d/all/%s_%d_0_0_%d_%s_%s_%.2f_%d", + shopId, isImageStr, returnNum, pageNum, sortType, sort, priceMin, pMax) + } else { + url = fmt.Sprintf("https://shop.kongfz.com/%d/all/%s_%d_0_0_%d_%s_%s_%.2f_%.2f", + shopId, isImageStr, returnNum, pageNum, sortType, sort, priceMin, priceMax) + } + // 发送请求 + response, err := fetchResponse(url, proxy) + if err != nil { + return nil, "", "", err + } + body, err := io.ReadAll(response.Body) + if err != nil { + return nil, "", "", err + } + doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body))) + // 全部商品数量 + num := doc.Find("div.crumbs-nav-main.clearfix").Find("span") + if match := regexp.MustCompile(`\d+`).FindString(num.Text()); match != "" { + goodsNum = match + } + // 商品页数 + pg := doc.Find("li.pull-right.page_num").Find("span") + _, split, found := strings.Cut(strings.TrimSpace(pg.Text()), "/") + if found { + pNum = split + } else { + log.Printf("未找到页数!") + } + infoDiv := doc.Find("div.list-content") + if infoDiv.Length() > 0 { + item := infoDiv.Find("div.item.clearfix") + for i := 0; i < item.Length(); i++ { + s := item.Eq(i) + book := BookInfo{} + // 书名 + book.BookName = strings.TrimSpace(s.Find("div.title a.link").Text()) + // 提取ISBN + book.ISBN = strings.TrimSpace(s.AttrOr("isbn", "")) + // 店铺ID + shopid, _ := strconv.Atoi(strings.TrimSpace(s.AttrOr("shopid", ""))) + book.ShopId = int64(shopid) + // 商品ID + itemid, _ := strconv.Atoi(strings.TrimSpace(s.AttrOr("itemid", ""))) + book.ItemId = int64(itemid) + book.DetailUrl = s.Find("div.item-img a.img-box").AttrOr("href", "") + books = append(books, book) + } + } + return books, goodsNum, pNum, nil +} + +// 获取商品信息通过商品详情链接 +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 + } + // 解析HTML文档 + document, err := goquery.NewDocumentFromReader(strings.NewReader(string(body))) + if err != nil { + return nil, err + } + // 获取商品信息的快递费 + fee, err := getBookDetailShippingFee(detailUrl, proxy) + if err != nil { + 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) { + titleSpan := li.Find("span.keywords-define-title") + contentSpan := li.Find("span.keywords-define-txt") + if contentSpan.Length() == 0 { + fmt.Printf("未找到指定的contentSpan信息") + } + titleText := strings.TrimSpace(titleSpan.Text()) + contentText := strings.TrimSpace(contentSpan.Text()) + titleText = strings.TrimSpace(titleText) + if strings.Contains(titleText, "作者") { + book.Author = cleanString(contentText) + } + if strings.Contains(titleText, "出版社") { + book.Publisher = contentText + } + if strings.Contains(titleText, "出版人") { + book.Publisher = contentText + } + if strings.Contains(titleText, "ISBN") { + book.ISBN = contentText + } + if strings.Contains(titleText, "出版时间") { + book.PublicationTime = validateDateFormat(contentText) + } + if strings.Contains(titleText, "版次") { + book.Edition = contentText + } + if strings.Contains(titleText, "装帧") { + book.BindingLayout = contentText + } + if strings.Contains(titleText, "开本") { + book.Format = contentText + } + if strings.Contains(titleText, "页数") { + book.Pages = contentText + } + if strings.Contains(titleText, "字数") { + book.Wordage = contentText + } + if strings.Contains(titleText, "纸张") { + book.Paper = contentText + } + if strings.Contains(titleText, "年代") { + book.Era = contentText + } + if strings.Contains(titleText, "刻印方式") { + book.EngravingMethod = contentText + } + if strings.Contains(titleText, "尺寸") { + book.Dimensions = contentText + } + if strings.Contains(titleText, "册数") { + book.VolumeNumber = contentText + } + }) + } 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()) + spanText = strings.TrimSpace(spanText) + if strings.Contains(li.Text(), "作者") { + book.Author = cleanString(li.Text()) + book.Author = strings.ReplaceAll(book.Author, "作者:", "") + book.Author = strings.ReplaceAll(book.Author, "著", "") + } + if strings.Contains(li.Text(), "出版社") { + book.Publisher = spanText + } + if strings.Contains(li.Text(), "出版时间") { + book.PublicationTime = validateDateFormat(spanText) + } + if strings.Contains(li.Text(), "ISBN") { + book.ISBN = spanText + } + if strings.Contains(li.Text(), "装帧") { + book.BindingLayout = spanText + } + if strings.Contains(li.Text(), "开本") { + book.Format = spanText + } + if strings.Contains(li.Text(), "纸张") { + book.Paper = spanText + } + if strings.Contains(li.Text(), "版次") { + book.Edition = spanText + } + if strings.Contains(li.Text(), "页数") { + book.Pages = spanText + } + if strings.Contains(li.Text(), "字数") { + book.Wordage = spanText + } + }) + } + + //图片 + var imgUrls []string + tpUl := document.Find("ul.lg-list") + tpUl.Find("img").Each(func(i int, s *goquery.Selection) { + dataImgUrl, exists := s.Attr("data-imgurl") + if exists && dataImgUrl != "" { + imgUrls = append(imgUrls, dataImgUrl) + } + }) + 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) { + 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 + if err != nil { + return "", fmt.Errorf("无效的店铺编码: %s", match[1]) + } + shopId = firstNum + secondNum, err := strconv.Atoi(match[2]) // 图书ID + if err != nil { + return "", fmt.Errorf("无效的图书编码: %s", match[2]) + } + itemId = secondNum + } + 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() + // 设置代理(如果有提供代理URL) + if proxy != "" { + request.Proxy(proxy) + } + // 设置超时和其他配置 + request.Timeout(30 * time.Second) + // 发送请求 + resp, body, errs := request.Get(shippingFeeUrl). + 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"). + End() + // 错误处理 + if len(errs) > 0 { + return "", fmt.Errorf("请求失败: %v", errs) + } + // 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP错误: %s", resp.Status) + } + calculateFee := struct { + ErrCode int `json:"errCode"` + ErrMessage string `json:"errMessage"` + Result struct { + FeeList []struct { + FreeCondition string `json:"freeCondition"` + ShippingID string `json:"shippingId"` + ShippingName string `json:"shippingName"` + ShippingValue string `json:"shippingValue"` + } `json:"feeList"` + FeeText string `json:"feeText"` + } `json:"result"` + Status bool `json:"status"` + }{} + + err := json.Unmarshal([]byte(body), &calculateFee) + if err != nil { + return "", fmt.Errorf("解析JSON失败: %v", err) + } + for _, fee := range calculateFee.Result.FeeList { + shippingFee = fee.ShippingValue + } + return shippingFee, nil +} + +// 公用发送请求方法 +func fetchResponse(url, proxy string) (*http.Response, error) { + log.Printf("调用的URL: %s", url) + maxRetries := cf.App.MaxRetryTimes + var detailsResp *http.Response + var errors []error + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + log.Printf("第 %d 次重试请求...", attempt) + // 重试前等待,使用指数退避策略 + waitTime := time.Duration(attempt*attempt) * cf.App.RateLimitDelay // 平方退避 + log.Printf("等待 %v 后重试", waitTime) + time.Sleep(waitTime) + } + if proxy != "" { + detailsResp, _, errors = gorequest.New(). + Get(url). + Proxy(proxy). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"). + Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(120 * time.Second). + End() + } + if proxy == "" { + detailsResp, _, errors = gorequest.New(). + Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"). + Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(120 * time.Second). + End() + } + // 检查是否需要重试 + shouldRetry := false + if len(errors) > 0 { + shouldRetry = true + log.Printf("请求失败 (尝试 %d/%d): %v", attempt+1, maxRetries+1, errors) + } else if detailsResp == nil { + shouldRetry = true + log.Printf("响应为空 (尝试 %d/%d)", attempt+1, maxRetries+1) + } else if detailsResp.StatusCode != http.StatusOK { + // 只对服务器错误进行重试,不对客户端错误重试 + if detailsResp.StatusCode >= 500 { + shouldRetry = true + } + log.Printf("HTTP状态码: %d,HTTP请求失败: %s (尝试 %d/%d)", detailsResp.StatusCode, detailsResp.Body, attempt+1, maxRetries+1) + } + // 如果不需要重试,跳出循环 + if !shouldRetry { + break + } + // 如果是最后一次尝试,不继续重试 + if attempt == maxRetries { + break + } + // 关闭响应体(如果存在) + if detailsResp != nil && detailsResp.Body != nil { + detailsResp.Body.Close() + } + } + // 检测请求是否错误 + if len(errors) > 0 { + var proxyAuthFailed bool + var timeoutError bool + var connectionError bool + for _, e := range errors { + errStr := e.Error() + if strings.Contains(errStr, "Proxy Authentication Required") { + proxyAuthFailed = true + } + if strings.Contains(errStr, "timeout") || strings.Contains(errStr, "i/o timeout") { + timeoutError = true + } + if strings.Contains(errStr, "connection") || strings.Contains(errStr, "connect") { + 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) + } + if detailsResp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", detailsResp.Status) + } + return detailsResp, nil +} + +// 获取销量榜商品列表(带有Out的都非官放标准接口) +func outGetTopGoodsListMsg(catId int, proxy string) ([]string, error) { + // 构建请求URL + url := fmt.Sprintf("https://item.kongfz.com/api/pc/getSellWellListDetail?page=1&pageSize=100&timeRank=2&catId=%d", catId) + // 创建HTTP客户端 + request := gorequest.New() + // 设置代理(如果有提供代理URL) + if proxy != "" { + request.Proxy(proxy) + } + // 设置超时和其他配置 + request.Timeout(30 * time.Second) + // 发送请求 + 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"). + Set("Referer", "https://item.kongfz.com/"). + End() + // 错误处理 + if len(errs) > 0 { + return nil, fmt.Errorf("请求失败: %v", errs) + } + // 检查HTTP状态码 + 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) + } + + var isbnList []string + for _, item := range bookDetailResponse.Result.Data { + if item.Isbn != "" { + isbnList = append(isbnList, item.Isbn) + } + } + isbnList = removeDuplicateISBNs(isbnList) + return isbnList, nil +} + +// 去除重复的ISBN +func removeDuplicateISBNs(isbns []string) []string { + seen := make(map[string]bool) + var result []string + for _, isbn := range isbns { + if !seen[isbn] { + seen[isbn] = true + result = append(result, isbn) + } + } + return result +} + +// 获取Document文档(带重试机制) +func fetchDocument(fetchMode, proxyType, username, password, machineCode, url string) (*goquery.Document, error) { + log.Printf("调用的URL: %s", url) + maxRetries := cf.App.MaxRetryTimes + var detailsResp *http.Response + var errors []error + var detailsBody []byte + for attempt := 0; attempt < maxRetries; attempt++ { + if attempt > 0 { + log.Printf("第 %d 次重试请求...", attempt) + // 重试前等待,使用指数退避策略 + waitTime := time.Duration(attempt*attempt) * cf.App.RateLimitDelay // 平方退避 + log.Printf("等待 %v 后重试", waitTime) + time.Sleep(waitTime) + } + if fetchMode == NotProxy { + detailsResp, _, errors = gorequest.New(). + Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"). + Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Timeout(120 * time.Second). + End() + } else if fetchMode == UseProxy { + detailsReq := XxProxyRequest(proxyType, username, password, machineCode). + Get(url). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"). + Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"). + Set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8"). + Set("Connection", "close") // 每次请求关闭连接 + detailsResp, _, errors = detailsReq.End() + } else { + errors = append(errors, fmt.Errorf("请求失败,选择模式错误!")) + } + // 检查是否需要重试 + shouldRetry := false + if len(errors) > 0 { + shouldRetry = true + log.Printf("请求失败 (尝试 %d/%d): %v", attempt+1, maxRetries+1, errors) + } else if detailsResp == nil { + shouldRetry = true + log.Printf("响应为空 (尝试 %d/%d)", attempt+1, maxRetries+1) + } else if detailsResp.StatusCode != http.StatusOK { + // 只对服务器错误进行重试,不对客户端错误重试 + if detailsResp.StatusCode >= 500 { + shouldRetry = true + } + log.Printf("HTTP状态码: %d,HTTP请求失败: %s (尝试 %d/%d)", detailsResp.StatusCode, detailsResp.Body, attempt+1, maxRetries+1) + } + // 如果不需要重试,跳出循环 + if !shouldRetry { + break + } + // 如果是最后一次尝试,不继续重试 + if attempt == maxRetries { + break + } + // 关闭响应体(如果存在) + if detailsResp != nil && detailsResp.Body != nil { + detailsResp.Body.Close() + } + } + // 检测请求是否错误 + if len(errors) > 0 { + var proxyAuthFailed bool + var timeoutError bool + var connectionError bool + for _, e := range errors { + errStr := e.Error() + if strings.Contains(errStr, "Proxy Authentication Required") { + proxyAuthFailed = true + } + if strings.Contains(errStr, "timeout") || strings.Contains(errStr, "i/o timeout") { + timeoutError = true + } + if strings.Contains(errStr, "connection") || strings.Contains(errStr, "connect") { + 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) + } + if detailsResp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP错误: %s", detailsResp.Status) + } + + detailsBody, err := io.ReadAll(detailsResp.Body) + if err != nil { + return nil, err + } + + return goquery.NewDocumentFromReader(strings.NewReader(string(detailsBody))) +} + +// 合并基本信息和详情 +func mergeBookDetails(bookItems []BookItem, detailResults map[string]*DetailResult) []BookInfo { + var books []BookInfo + + for _, item := range bookItems { + book := item.Book + + // 如果有详情且获取成功,补充详细信息 + if item.HasDetail && item.DetailURL != "" { + if result, exists := detailResults[item.DetailURL]; exists && result.Error == nil && result.Doc != nil { + detailsDiv := result.Doc.Find("div.detail-lists.clear-fix") + if detailsDiv.Length() > 0 { + selection := detailsDiv.Find("li") + book.Author = extractNormalDetails(selection, "作者") + book.Publisher = extractNormalDetails(selection, "出版社") + detailsTime := extractNormalDetails(selection, "出版时间") + if detailsTime != "" { + formats := []string{"2006-01", "2006-01-02", "2006"} + for _, format := range formats { + if t, err := time.Parse(format, detailsTime); err == nil { + book.PublicationTime = t.Unix() + break + } + } + } + book.ISBN = extractNormalDetails(selection, "ISBN") + book.Edition = extractNormalDetails(selection, "版次") + book.FixPrice = extractNormalDetails(selection, "定价") + book.BindingLayout = extractNormalDetails(selection, "装帧") + book.Format = extractNormalDetails(selection, "开本") + book.Pages = extractNormalDetails(selection, "页数") + book.Wordage = extractNormalDetails(selection, "字数") + } + } + } + + books = append(books, book) + } + return books +} + +// 根据url获取单个图书详情信息 +func getUrlBookDetails(fetchMode, proxyType, username, password, machineCode, url string) (books []BookInfo, err error) { + document, err := fetchDocument(fetchMode, proxyType, username, password, machineCode, url) + if err != nil { + return nil, err + } + + fee, err := FetchBookDetailsShippingFee(url) + if err != nil { + 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) { + titleSpan := li.Find("span.keywords-define-title") + contentSpan := li.Find("span.keywords-define-txt") + if contentSpan.Length() == 0 { + fmt.Printf("未找到指定的contentSpan信息") + } + titleText := strings.TrimSpace(titleSpan.Text()) + contentText := strings.TrimSpace(contentSpan.Text()) + titleText = strings.TrimSpace(titleText) + if strings.Contains(titleText, "作者") { + book.Author = cleanString(contentText) + } + if strings.Contains(titleText, "出版社") { + book.Publisher = contentText + } + if strings.Contains(titleText, "出版人") { + book.Publisher = contentText + } + if strings.Contains(titleText, "ISBN") { + book.ISBN = contentText + } + if strings.Contains(titleText, "出版时间") { + book.PublicationTime = validateDateFormat(contentText) + } + if strings.Contains(titleText, "版次") { + book.Edition = contentText + } + if strings.Contains(titleText, "装帧") { + book.BindingLayout = contentText + } + if strings.Contains(titleText, "开本") { + book.Format = contentText + } + if strings.Contains(titleText, "页数") { + book.Pages = contentText + } + if strings.Contains(titleText, "字数") { + book.Wordage = contentText + } + if strings.Contains(titleText, "纸张") { + book.Paper = contentText + } + if strings.Contains(titleText, "年代") { + book.Era = contentText + } + if strings.Contains(titleText, "刻印方式") { + book.EngravingMethod = contentText + } + if strings.Contains(titleText, "尺寸") { + book.Dimensions = contentText + } + if strings.Contains(titleText, "册数") { + book.VolumeNumber = contentText + } + }) + } 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()) + spanText = strings.TrimSpace(spanText) + if strings.Contains(li.Text(), "作者") { + book.Author = cleanString(li.Text()) + book.Author = strings.ReplaceAll(book.Author, "作者:", "") + book.Author = strings.ReplaceAll(book.Author, "著", "") + } + if strings.Contains(li.Text(), "出版社") { + book.Publisher = spanText + } + if strings.Contains(li.Text(), "出版时间") { + book.PublicationTime = validateDateFormat(spanText) + } + if strings.Contains(li.Text(), "ISBN") { + book.ISBN = spanText + } + if strings.Contains(li.Text(), "装帧") { + book.BindingLayout = spanText + } + if strings.Contains(li.Text(), "开本") { + book.Format = spanText + } + if strings.Contains(li.Text(), "纸张") { + book.Paper = spanText + } + if strings.Contains(li.Text(), "版次") { + book.Edition = spanText + } + if strings.Contains(li.Text(), "页数") { + book.Pages = spanText + } + if strings.Contains(li.Text(), "字数") { + book.Wordage = spanText + } + }) + } + + //图片 + var imgUrls []string + tpUl := document.Find("ul.lg-list") + tpUl.Find("img").Each(func(i int, s *goquery.Selection) { + dataImgUrl, exists := s.Attr("data-imgurl") + if exists && dataImgUrl != "" { + imgUrls = append(imgUrls, dataImgUrl) + } + }) + 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) + // 快递费 + if fee != "" { + if fee == "包邮" { + book.ExpressDeliveryFee = "0" + } else { + book.ExpressDeliveryFee = fee + } + } + books = append(books, book) + return books, nil +} + +// 获取店铺页面的所有快递费用 +func FetchProductInfoWithChromedp(url string) (*ProductResponse, error) { + // 创建上下文 + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"), + // 禁用图片加载,加快速度 + chromedp.Flag("blink-settings", "imagesEnabled=false"), + ) + // 启动浏览器 + allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer cancel() + // 创建浏览器上下文 + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + // 设置超时 + ctx, cancel = context.WithTimeout(ctx, 60*time.Second) + defer cancel() + log.Printf("[CHROMEDP] 开始导航到URL: %s", url) + // 第一步:先导航到页面并等待完全加载 + err := chromedp.Run(ctx, + // 导航到目标页面 + chromedp.Navigate(url), + + // 等待页面初步加载 + chromedp.WaitReady("body", chromedp.ByQuery), + chromedp.Sleep(3*time.Second), + + // 第二步:刷新页面,重新加载动态内容 + chromedp.Reload(), + + // 等待刷新后页面加载 + chromedp.WaitReady("body", chromedp.ByQuery), + chromedp.Sleep(5*time.Second), + + // 等待关键元素出现 + chromedp.WaitVisible(".item.clearfix", chromedp.ByQuery), + + // 滚动页面以触发动态加载 + chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight / 3)`, nil), + chromedp.Sleep(2*time.Second), + chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight / 2)`, nil), + chromedp.Sleep(2*time.Second), + chromedp.Evaluate(`window.scrollTo(0, document.body.scrollHeight)`, nil), + chromedp.Sleep(3*time.Second), + chromedp.Evaluate(`window.scrollTo(0, 0)`, nil), + chromedp.Sleep(1*time.Second), + ) + if err != nil { + log.Printf("[CHROMEDP] 页面导航失败: %v", err) + return nil, fmt.Errorf("页面导航失败: %v", err) + } + log.Printf("[CHROMEDP] 页面导航完成,开始提取数据") + var evalResult map[string]interface{} + // 执行任务 + err = chromedp.Run(ctx, + // 导航到目标页面 + chromedp.Navigate(url), + + // 执行 JavaScript 来提取商品信息 + chromedp.Evaluate(`(function() { + console.log("开始提取商品信息..."); + try { + let productData = []; + // 查找所有商品项 - 针对孔夫子旧书网的特定选择器 + const itemSelectors = [ + '.item.clearfix', + '.list-item', + '.product-item', + '.goods-item', + '[class*="item"]' + ]; + let items = []; + for (let selector of itemSelectors) { + const found = document.querySelectorAll(selector); + if (found.length > 0) { + items = found; + console.log("使用选择器:", selector, "找到项目数:", items.length); + break; + } + } + + if (items.length === 0) { + // 如果没有找到特定选择器,尝试查找任何看起来像商品的项目 + items = document.querySelectorAll('div[class*="item"], li[class*="item"]'); + console.log("使用通用选择器找到项目数:", items.length); + } + console.log("总共找到商品项:", items.length); + // 遍历每个商品项 + items.forEach((item, index) => { + // 提取 itemid + let itemId = ''; + + // 方法1: 从元素属性获取 + if (item.hasAttribute('itemid')) { + itemId = item.getAttribute('itemid'); + } + + // 方法2: 从数据属性获取 + if (!itemId && item.hasAttribute('data-itemid')) { + itemId = item.getAttribute('data-itemid'); + } + + // 方法3: 从ID属性获取 + if (!itemId && item.hasAttribute('id')) { + const id = item.getAttribute('id'); + if (id.includes('item') || id.includes('product')) { + itemId = id; + } + } + + // 方法4: 从链接中提取itemid + if (!itemId) { + const links = item.querySelectorAll('a[href*="item"], a[href*="product"]'); + for (let link of links) { + const href = link.getAttribute('href'); + if (href) { + const itemMatch = href.match(/(?:item|product)[_-]?(\d+)/i); + if (itemMatch) { + itemId = itemMatch[1]; + break; + } + } + } + } + + // 方法5: 从子元素中查找itemid + if (!itemId) { + const childWithItemId = item.querySelector('[itemid], [data-itemid]'); + if (childWithItemId) { + if (childWithItemId.hasAttribute('itemid')) { + itemId = childWithItemId.getAttribute('itemid'); + } else if (childWithItemId.hasAttribute('data-itemid')) { + itemId = childWithItemId.getAttribute('data-itemid'); + } + } + } + + // 提取书名 + let bookName = ''; + const titleSelectors = [ + '.title a', + '.book-title', + '.item-title', + '.name a', + 'h3 a', + 'h4 a', + '.link', + 'a[title]' + ]; + + for (let selector of titleSelectors) { + const titleEl = item.querySelector(selector); + if (titleEl) { + bookName = titleEl.textContent.trim(); + if (bookName && bookName.length > 1) { + break; + } + } + } + + // 如果没找到,尝试在item内查找任何文本作为书名 + if (!bookName) { + const textContent = item.textContent; + // 尝试提取看起来像书名的文本(较长的文本块) + const lines = textContent.split('\n').map(line => line.trim()).filter(line => line.length > 5); + if (lines.length > 0) { + bookName = lines[0]; + } + } + + // 提取价格 + let price = ''; + const priceSelectors = [ + '.price span', + '.price .bold', + '.current-price', + '.sell-price', + '.cost', + '.money', + '[class*="price"]', + 'strong' + ]; + + for (let selector of priceSelectors) { + const priceEl = item.querySelector(selector); + if (priceEl) { + let priceText = priceEl.textContent.trim(); + // 清理价格文本,保留数字和小数点 + priceText = priceText.replace(/[^\d\.]/g, ''); + if (priceText && !isNaN(parseFloat(priceText))) { + price = '¥' + priceText; + break; + } + } + } + + // 如果没找到价格,尝试在文本中查找价格模式 + if (!price) { + const itemText = item.textContent; + const priceMatch = itemText.match(/[¥¥]?\s*(\d+\.?\d*)/); + if (priceMatch) { + price = '¥' + priceMatch[1]; + } + } + + // 提取快递费用 + let shippingFee = ''; + const shippingSelectors = [ + '.ship-fee', + '.shipping-fee', + '.express-fee', + '.freight', + '.postage', + '[class*="fee"]', + '[class*="快递"]', + '[class*="运费"]' + ]; + + for (let selector of shippingSelectors) { + const shippingEl = item.querySelector(selector); + if (shippingEl) { + shippingFee = shippingEl.textContent.trim(); + if (shippingFee) break; + } + } + + // 如果没找到运费元素,尝试在文本中查找运费关键词 + if (!shippingFee) { + const itemText = item.textContent; + const feeMatches = itemText.match(/(运费[::]\s*[^\s\n]+)|(快递[::]\s*[^\s\n]+)|(邮费[::]\s*[^\s\n]+)/); + if (feeMatches) { + shippingFee = feeMatches[0]; + } else if (itemText.includes('包邮') || itemText.includes('免运费')) { + shippingFee = '包邮'; + } else { + shippingFee = '运费待确认'; + } + } + + // 清理书名(移除过长的文本) + if (bookName && bookName.length > 100) { + bookName = bookName.substring(0, 100) + '...'; + } + + // 如果还没有itemid,生成一个基于索引的ID + if (!itemId) { + itemId = 'item_' + (index + 1); + } + + // 添加到数据中 + productData.push({ + itemId: itemId, + bookName: bookName || '商品${index + 1}', + price: price || '价格待确认', + shippingFee: shippingFee + }); + }); + + // 过滤掉明显无效的数据 + productData = productData.filter(item => + (item.bookName !== '商品1' || item.price !== '价格待确认') && + item.bookName && item.bookName.length > 0 + ); + + console.log("处理后商品数量:", productData.length); + if (productData.length > 0) { + console.log("前3个商品示例:", productData.slice(0, 3)); + } + + return { + success: true, + message: "成功提取商品信息", + data: productData + }; + + } catch(error) { + console.error("提取商品信息时出错:", error); + return { + success: false, + message: "提取商品信息时出错: " + error.message, + data: [] + }; + } + })()`, &evalResult), + ) + + if err != nil { + return nil, fmt.Errorf("chromedp执行失败: %v", err) + } + + // 处理 JavaScript 执行结果 + response := &ProductResponse{} + + // 转换结果数据 + if success, ok := evalResult["success"].(bool); ok { + response.Success = success + } + if message, ok := evalResult["message"].(string); ok { + response.Message = message + } + if data, ok := evalResult["data"].([]interface{}); ok { + for _, item := range data { + if productMap, ok := item.(map[string]interface{}); ok { + product := ProductInfo{} + if itemId, ok := productMap["itemId"].(string); ok { + product.ItemID = itemId + } + if bookName, ok := productMap["bookName"].(string); ok { + product.BookName = bookName + } + if price, ok := productMap["price"].(string); ok { + product.Price = price + } + if shippingFee, ok := productMap["shippingFee"].(string); ok { + info := extractShippingInfo(shippingFee) + product.ShippingFee = info + } + response.Data = append(response.Data, product) + } + } + } + + return response, nil +} + +// 获取详情页的快递费用 +func FetchBookDetailsShippingFee(url string) (string, error) { + // 创建上下文 + opts := append(chromedp.DefaultExecAllocatorOptions[:], + chromedp.Flag("headless", true), + chromedp.Flag("disable-gpu", true), + chromedp.Flag("no-sandbox", true), + chromedp.Flag("disable-dev-shm-usage", true), + chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"), + // 禁用图片加载,加快速度 + chromedp.Flag("blink-settings", "imagesEnabled=false"), + ) + // 启动浏览器 + allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer cancel() + // 创建浏览器上下文 + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + // 设置超时 + ctx, cancel = context.WithTimeout(ctx, 60*time.Second) + defer cancel() + log.Printf("[CHROMEDP] 开始导航到URL: %s", url) + var result string + + // 增强的 JavaScript 脚本 + enhancedJSScript := ` + (function() { + // 查找包含快递费用的元素 + const elements = document.querySelectorAll('*'); + let shippingFee = null; + + elements.forEach(element => { + if (element.textContent.includes('快递¥')) { + const match = element.textContent.match(/快递¥(\d+\.\d{2})/); + if (match) { + shippingFee = match[1]; + console.log('找到快递费用: ¥' + shippingFee); + } + } + // 匹配格式2: 快递:包邮 + else if (element.textContent.includes('快递:包邮') || element.textContent.includes('快递:包邮')) { + shippingFee = '包邮'; + console.log('找到快递费用: 包邮'); + } + // 匹配格式3: 快递 包邮 (包含空格变体) + else if (element.textContent.match(/快递[::\s]包邮/)) { + shippingFee = '包邮'; + console.log('找到快递费用: 包邮'); + } + }); + + if (!shippingFee) { + // 如果没找到,尝试搜索整个页面 + const pageText = document.body.innerText; + const match = pageText.match(/快递¥(\d+\.\d{2})/); + if (match) { + shippingFee = match[1]; + console.log('找到快递费用: ¥' + shippingFee); + } + // 尝试匹配包邮 + else if (pageText.match(/快递[::\s]包邮/)) { + shippingFee = '包邮'; + console.log('找到快递费用: 包邮'); + } + // 尝试其他包邮表述 + else if (pageText.includes('包邮') && pageText.includes('快递')) { + shippingFee = '包邮'; + console.log('找到快递费用: 包邮'); + } + else { + console.log('未找到快递费用信息'); + } + } + return shippingFee; + })();` + // 先导航到页面并等待完全加载 + err := chromedp.Run(ctx, + // 导航到目标页面 + chromedp.Navigate(url), + // 刷新页面,重新加载动态内容 + chromedp.Reload(), + // 等待刷新后页面加载 + chromedp.WaitReady("body", chromedp.ByQuery), + chromedp.Sleep(8*time.Second), + // 执行 JavaScript 来提取商品信息 + chromedp.Evaluate(enhancedJSScript, &result), + ) + if err != nil { + log.Printf("[CHROMEDP] 页面导航失败: %v", err) + return "", fmt.Errorf("页面导航失败: %v", err) + } + log.Printf("[CHROMEDP] 页面导航完成,开始提取数据") + + if err != nil { + return "", fmt.Errorf("chromedp执行失败: %v", err) + } + return result, nil +} + +// 获取孔网实拍图(查询商品列表) +func getKFZSPTImageURL(proxyType, username, password, machineCode, isbn string) (*BookInfo, error) { + log.Printf("[DEBUG] ", isbn, " 获取图书ISBN: %s", isbn) + url := fmt.Sprintf("%s?dataType=0&keyword=%s&page=1&size=%d&sortType=7&actionPath=quality,sortType&quality=85~&quaSelect=2&userArea=13003000000", cf.API.ProductSearchURL, isbn, cf.App.Size) + + req := XxProxyRequest(proxyType, username, password, machineCode). + Get(url). + Set("User-Agent", cf.App.DefaultUserAgent). + Set("Accept", "*/*") + resp, body, errs := req.End() + log.Printf("[DEBUG] 获取图书列表 URL: %s", url) + + // 只在有响应内容时才打印,避免日志过长 + if len(body) > 0 && len(body) < 500 { + log.Printf("[DEBUG] 获取图书列表响应: %s", body) + } else if len(body) > 0 { + log.Printf("[DEBUG] 获取图书列表响应长度: %d 字符", len(body)) + } else { + log.Printf("[DEBUG] 获取图书列表响应为空") + } + + // 处理请求错误 + if len(errs) > 0 { + // 检查是否是代理认证失败 + var proxyAuthFailed bool + for _, e := range errs { + if strings.Contains(e.Error(), "Proxy Authentication Required") { + proxyAuthFailed = true + break + } + } + if proxyAuthFailed { + return nil, fmt.Errorf("代理认证失败") + } + return nil, fmt.Errorf("查询请求失败: %v", errs) + } + + // 检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + space := strings.TrimSpace(resp.Status) + return nil, fmt.Errorf("HTTP错误: %s", space) + } + + // 解析响应 + var apiResp struct { + Status int `json:"status"` + ErrType string `json:"errType"` + Message string `json:"message"` + SystemTime int64 `json:"systemTime"` + Data struct { + ItemResponse struct { + Total int `json:"total"` + List []struct { + Title string `json:"title"` + ImgUrl string `json:"imgUrl"` + ImgBigUrl string `json:"imgBigUrl"` + ItemId int64 `json:"itemId"` + ShopId int64 `json:"shopId"` + TplRecords []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"tplRecords"` + } `json:"list"` + } `json:"itemResponse"` + } `json:"data"` + } + + if err := json.Unmarshal([]byte(body), &apiResp); err != nil { + return nil, fmt.Errorf("解析JSON失败: %w", err) + } + + // 如果找到商品,返回图片URL + if apiResp.Data.ItemResponse.Total > 0 && len(apiResp.Data.ItemResponse.List) > 0 { + // 确定起始索引 + var startIndex int + if cf.App.Size >= apiResp.Data.ItemResponse.Total { + startIndex = apiResp.Data.ItemResponse.Total - 1 + } else { + startIndex = cf.App.Size - 1 + } + + // 从指定索引开始,向前查找有效图片,最多重试3次 + for attempt := 0; attempt < 3; attempt++ { + currentIndex := startIndex - attempt + + // 检查索引是否有效 + if currentIndex < 0 || currentIndex >= len(apiResp.Data.ItemResponse.List) { + log.Printf("[DEBUG] 索引 %d 超出范围,跳过", currentIndex) + continue + } + + item := apiResp.Data.ItemResponse.List[currentIndex] + + // 检查图片URL是否存在 + if item.ImgBigUrl == "" { + log.Printf("[DEBUG] 索引 %d 的图片URL为空,跳过", currentIndex) + continue + } + + info := &BookInfo{} + info.BookName = item.Title + info.BookPicS = item.ImgUrl + info.ItemId = item.ItemId + info.ShopId = item.ShopId + + // 安全地获取TplRecords中的值 + if len(item.TplRecords) > 0 { + info.Author = item.TplRecords[0].Value + } + if len(item.TplRecords) > 1 { + info.Publisher = item.TplRecords[1].Value + } + if len(item.TplRecords) > 2 { + info.PublicationTime = validateDateFormat(item.TplRecords[2].Value) + } + if len(item.TplRecords) > 3 { + info.BindingLayout = item.TplRecords[3].Value + } + + // 成功时重置代理失败计数器 + resetProxyFailCount() + return info, nil + } + // 如果所有尝试都失败了,返回错误 + log.Printf("[WARN] 经过3次尝试,未找到有效图片") + return nil, fmt.Errorf("未找到有效图片,已尝试3次") + } + return nil, nil +} + +// 获取孔网官图(查询图书条目) +func getKFZGTImageURL(proxyType, username, password, machineCode, isbn string) (*BookInfo, error) { + url := fmt.Sprintf("%s?keyword=%s", cf.API.BookSearchURL, isbn) + + info := &BookInfo{} + + req := XxProxyRequest(proxyType, username, password, machineCode). + Get(url). + Set("User-Agent", cf.App.DefaultUserAgent). + Set("Accept", "*/*") + resp, body, errs := req.End() + log.Printf("[DEBUG] 获取图书条目 URL: %s", url) + // 只在有响应内容时才打印,避免日志过长 + if len(body) > 0 && len(body) < 500 { + log.Printf("[DEBUG] 获取图书条目响应: %s", body) + } else if len(body) > 0 { + log.Printf("[DEBUG] 获取图书条目响应长度: %d 字符", len(body)) + } else { + log.Printf("[DEBUG] 获取图书条目响应为空") + } + + // 处理请求错误 + if len(errs) > 0 { + // 检查是否是代理相关错误 + var isProxyError bool + var errorDetails []string + for _, e := range errs { + errorStr := e.Error() + errorDetails = append(errorDetails, errorStr) + if strings.Contains(errorStr, "Proxy Authentication Required") || + strings.Contains(errorStr, "connectex: A connection attempt failed") || + strings.Contains(errorStr, "connectex: No connection could be made") || + strings.Contains(errorStr, "proxyconnect tcp") || + strings.Contains(errorStr, "timeout") || + strings.Contains(errorStr, "connection refused") { + isProxyError = true + } + } + + log.Printf("[ERROR] 请求错误详情: %v", errorDetails) + + if isProxyError { + // 处理代理失败 + return nil, fmt.Errorf("代理连接失败") + } + + return nil, fmt.Errorf("查询请求失败: %v", errs) + } + + //检查HTTP状态码 + if resp.StatusCode != http.StatusOK { + space := strings.TrimSpace(resp.Status) + return nil, fmt.Errorf("HTTP错误: %s", space) + } + + // 解析响应 + var apiResp struct { + Status int `json:"status"` + ErrType string `json:"errType"` + Message string `json:"message"` + SystemTime int64 `json:"systemTime"` + Data struct { + ItemResponse struct { + Total int `json:"total"` + List []struct { + BookName string `json:"bookName"` + Mid int64 `json:"mid"` + ImgUrlEntity struct { + BigImgUrl string `json:"bigImgUrl"` + } `json:"imgUrlEntity"` + BookShowInfo []string `json:"bookShowInfo"` + } `json:"list"` + } `json:"itemResponse"` + } `json:"data"` + } + + if err := json.Unmarshal([]byte(body), &apiResp); err != nil { + return nil, fmt.Errorf("解析JSON失败: %w", err) + } + + // 如果找到条目,返回图片URL + if apiResp.Data.ItemResponse.Total > 0 && len(apiResp.Data.ItemResponse.List) > 0 { + list := apiResp.Data.ItemResponse.List[0] + info := list.BookShowInfo + + bookItem := &BookInfo{ + BookName: list.BookName, + BookPic: list.ImgUrlEntity.BigImgUrl, + Mid: list.Mid, + } + + // 根据长度安全填充字段 + if len(info) > 0 { + bookItem.Author = info[0] + } + if len(info) > 1 { + bookItem.Publisher = info[1] + } + if len(info) > 2 { + bookItem.PublicationTime = validateDateFormat(info[2]) + } + if len(info) > 3 { + bookItem.BindingLayout = info[3] + } + if len(info) > 4 { + bookItem.FixPrice = info[4] + } else { + log.Printf("[WARN] BookShowInfo 长度不足 (仅 %d 项): %v", len(info), info) + } + + fmt.Println("bookItem: ", bookItem) + return bookItem, nil + } + + return info, nil +} + +// 替换所有空白字符为空格 +func cleanString(s string) string { + s = strings.ReplaceAll(s, "\n", "") + s = strings.ReplaceAll(s, "\r", "") + s = strings.ReplaceAll(s, "\t", "") + s = strings.ReplaceAll(s, " ", "") + return removeDuplicates(s) +} + +// 字符串去重 +func removeDuplicates(s string) string { + seen := make(map[rune]bool) + var result strings.Builder + for _, r := range s { + if !seen[r] { + seen[r] = true + result.WriteRune(r) + } + } + return result.String() +} + +// 检测快递费用格式 +func extractShippingInfo(shippingFee string) string { + // 匹配 "快递¥数字" 格式 + re1 := regexp.MustCompile(`快递¥(\d+\.?\d*)`) + // 匹配 "包邮" 格式 + re2 := regexp.MustCompile(`(包邮)`) + + if matches := re1.FindStringSubmatch(shippingFee); len(matches) > 1 { + return matches[1] // 返回数字部分 + } + + if matches := re2.FindStringSubmatch(shippingFee); len(matches) > 1 { + return "0" // 返回"包邮" + } + + return "" +} + +// 改进字段 +func extractNormalDetails(s *goquery.Selection, fieldName string) string { + var info string + s.Each(func(i int, selection *goquery.Selection) { + if strings.Contains(selection.Text(), fieldName) { + all := strings.ReplaceAll(selection.Text(), " ", "") + all = strings.ReplaceAll(all, "\n", "") + all = strings.ReplaceAll(all, " ", "") + split := strings.SplitN(all, ":", 2) + if len(split) > 1 { + info = split[1] + } else { + return + } + } + }) + return info +} + +// 改进的字段提取函数 +func extractNormalText(s *goquery.Selection, fieldName string) string { + // 在左右两个分区中查找 + selectors := []string{"div.f_left", "div.f_right"} + for _, selector := range selectors { + fieldItem := s.Find(selector + " div.normal-item").FilterFunction(func(i int, sel *goquery.Selection) bool { + title := sel.Find("span.normal-title").Text() + return strings.Contains(title, fieldName) + }) + + if fieldItem.Length() > 0 { + text := fieldItem.Find("span.normal-text").Text() + return strings.TrimSpace(text) + } + } + return "" +} + +// 重置代理失败计数器(成功时调用) +func resetProxyFailCount() { + tailProxyMu.Lock() + defer tailProxyMu.Unlock() + proxyFailCount = 0 +} + +// 验证日期格式 +func validateDateFormat(dateStr string) int64 { + // 去除前后空格 + dateStr = strings.TrimSpace(dateStr) + + // 替换各种分隔符为统一的分隔符"-" + dateStr = regexp.MustCompile(`[/_\\.,\s]+`).ReplaceAllString(dateStr, "-") + + // 处理纯年份格式 (4位数字) + if regexp.MustCompile(`^\d{4}$`).MatchString(dateStr) { + dateStr += "-01-01" + } + + // 处理年月格式 (4位数字-1或2位数字) + if matches := regexp.MustCompile(`^(\d{4})-(\d{1,2})$`).FindStringSubmatch(dateStr); len(matches) == 3 { + year := matches[1] + month := matches[2] + if len(month) == 1 { + month = "0" + month + } + if monthNum, _ := strconv.Atoi(month); monthNum >= 1 && monthNum <= 12 { + dateStr = year + "-" + month + "-01" + } else { + return 0 + } + } + + // 处理年月日格式 (4位数字-1或2位数字-1或2位数字) + if matches := regexp.MustCompile(`^(\d{4})-(\d{1,2})-(\d{1,2})`).FindStringSubmatch(dateStr); len(matches) >= 4 { + year := matches[1] + month := matches[2] + day := matches[3] + + // 标准化月份和日期为两位数 + if len(month) == 1 { + month = "0" + month + } + if len(day) == 1 { + day = "0" + day + } + + dateStr = year + "-" + month + "-" + day + } + + // 加载上海时区 + loc, err := time.LoadLocation("Asia/Shanghai") + if err != nil { + // 如果加载时区失败,使用UTC+8作为后备方案 + loc = time.FixedZone("CST", 8*60*60) + } + + // 尝试解析为标准日期格式,并指定上海时区 + parsedTime, err := time.ParseInLocation("2006-01-02", dateStr, loc) + if err != nil { + return 0 + } + + // 返回秒级时间戳(UTC时间,但解析时已经考虑了时区偏移) + return parsedTime.Unix() +} + +// 代理请求(小象代理) +func XxProxyRequest(proxyType, username, password, machineCode string) *gorequest.SuperAgent { + proxyManagerOnce.Do(func() { + log.Printf("[INFO] 初始化代理管理器,DLL路径: %s", cf.Proxy.ProxyFilePath) + + // 检查DLL文件是否存在 + if _, err := os.Stat(cf.Proxy.ProxyFilePath); os.IsNotExist(err) { + // 尝试在可执行文件目录查找 + exePath, _ := os.Executable() + exeDir := filepath.Dir(exePath) + dllPath := filepath.Join(exeDir, cf.Proxy.ProxyFilePath) + + if _, err := os.Stat(dllPath); err == nil { + cf.Proxy.ProxyFilePath = dllPath + } else { + proxyManagerInitErr = fmt.Errorf("代理DLL文件不存在: %s (也尝试了: %s)", cf.Proxy.ProxyFilePath, dllPath) + return + } + } + + globalProxyManager, proxyManagerInitErr = NewProxyConfigManager(cf.Proxy.ProxyFilePath) + if proxyManagerInitErr != nil { + log.Printf("[ERROR] 代理管理器初始化失败: %v", proxyManagerInitErr) + } else { + log.Printf("[INFO] 代理管理器初始化成功") + } + }) + typeManager, err := globalProxyManager.ProxyTypeManager( + proxyType, + username, + password, + machineCode, + ) + if err != nil { + fmt.Printf("获取代理服务器信息失败: %v", err) + } + return gorequest.New().Proxy(typeManager).Timeout(120*time.Second).Retry(2, 3*time.Second) +} + +// 初始化 +func initializeConfig(config Config) { + // 设置全局配置 + cf = config +} + +// 连接数据库(选品) +func connectDBXP() (*sql.DB, error) { + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", + cf.Database.Username, cf.Database.Password, cf.Database.Host, cf.Database.Name)) + if err != nil { + return nil, fmt.Errorf("打开数据库连接失败: %v", err) + } + // 设置连接池参数 + db.SetMaxOpenConns(20) // 最大打开连接数 + db.SetMaxIdleConns(10) // 最大空闲连接数 + err = db.Ping() + if err != nil { + return nil, fmt.Errorf("数据库连接测试失败: %v", err) + } + return db, nil +} + +// 从数据库随机获取一个可用账号 +func getRandomAccount() (*AccountCredential, error) { + mu.RLock() + defer mu.RUnlock() + // sql语句 + query := ` + SELECT id, credential_account, credential_password, token + FROM t_credential + WHERE credential_type = 'KWCredential' + AND status = 0 + AND is_del = 0 + ORDER BY RAND() + LIMIT 1` + + db, err := connectDBXP() + if err != nil { + return nil, err + } + var ac AccountCredential + err = db.QueryRow(query).Scan(&ac.ID, &ac.Username, &ac.Password, &ac.Token) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("没有可用的账号") + } + return nil, fmt.Errorf("查询账号失败: %v", err) + } + return &ac, nil +} + +// 修改账号为不可用 +func updateAccountInvalid(id int64) error { + mu.RLock() + defer mu.RUnlock() + db, err := connectDBXP() + if err != nil { + return err + } + query := `UPDATE t_credential SET is_del = 1 WHERE id = ?` + _, err = db.Exec(query, id) + return fmt.Errorf("更新失败: %v", err) +} + +// 修改账号为需要验证 +func updateAccountNeedVerify(id int64) error { + mu.RLock() + defer mu.RUnlock() + db, err := connectDBXP() + if err != nil { + return err + } + query := `UPDATE t_credential SET status = 1 WHERE id = ?` + _, err = db.Exec(query, id) + return err +} + +// 更新账号token +func updateAccountToken(id int64, token string) error { + mu.RLock() + defer mu.RUnlock() + db, err := connectDBXP() + if err != nil { + return err + } + query := `UPDATE t_credential SET token = ? WHERE id = ?` + _, err = db.Exec(query, token, id) + return err +} + +// 登录(带有Out的都非官方标准接口) +// +//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, + } + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: resp, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取用户信息(带有Out的都非官方标准接口) +// +//export OutGetUserMsg +func OutGetUserMsg(token *C.char) *C.char { + goToken := C.GoString(token) + userInfo, err := outGetUserMsg(goToken) + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: userInfo, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取商品模版(带有Out的都非官方标准接口) +// +//export OutGetGoodsTplMsg +func OutGetGoodsTplMsg(token, itemId, proxy *C.char) *C.char { + goToken := C.GoString(token) + goItemId := C.GoString(itemId) + goProxy := C.GoString(proxy) + info, err := outGetGoodsTplMsg(goToken, goItemId, goProxy) + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: info, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取商品列表-已登的店铺(带有Out的都非官方标准接口) +// +//export OutGetGoodsListMsgFromSelfShop +func OutGetGoodsListMsgFromSelfShop(token, proxy, itemSn, priceMin, priceMax *C.char, startCreateTime C.int, + endCreateTime C.int, requestType *C.char, isItemSnEqual C.int, page C.int, size C.int) *C.char { + goToken := C.GoString(token) + goProxy := C.GoString(proxy) + goItemSn := C.GoString(itemSn) + goPriceMin := C.GoString(priceMin) + goPriceMax := C.GoString(priceMax) + goStartCreateTime := int(startCreateTime) + goEndCreateTime := int(endCreateTime) + goRequestType := C.GoString(requestType) + goIsItemSnEqual := int(isItemSnEqual) + goPage := int(page) + goSize := int(size) + info, err := outGetGoodsListMsgFromSelfShop(goToken, goProxy, goItemSn, goPriceMin, goPriceMax, goStartCreateTime, goEndCreateTime, goRequestType, goIsItemSnEqual, goPage, goSize) + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: info, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 新增商品(带有Out的都非官方标准接口) +// +//export OutAddGoods +func OutAddGoods(token, proxy, formData *C.char) *C.char { + goToken := C.GoString(token) + goProxy := C.GoString(proxy) + goFormData := C.GoString(formData) + info, err := outAddGoods(goToken, goProxy, goFormData) + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: info, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取商品图片(带有Out的都非官方标准接口) +// +//export OutGetImageByIsbn +func OutGetImageByIsbn(token, isbn, proxy *C.char, isLiveImage C.int, isReturnMsg C.int) *C.char { + goToken := C.GoString(token) + goIsbn := C.GoString(isbn) + goProxy := C.GoString(proxy) + goIsLiveImage := int(isLiveImage) + goIsReturnMsg := int(isReturnMsg) + bookInfo, err := outGetImageByIsbn(goToken, goIsbn, goProxy, goIsLiveImage, goIsReturnMsg) + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: bookInfo, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取商品列表通过店铺ID(带有Out的都非官方标准接口) +// +//export OutGetGoodsListMsgByShopId +func OutGetGoodsListMsgByShopId(shopId C.int, proxy *C.char, isImage C.int, sortType *C.char, sort *C.char, priceMin C.float, priceMax C.float, pageNum, returnNum C.int) *C.char { + goShopId := int(shopId) + goProxy := C.GoString(proxy) + goIsImage := int(isImage) + goSortType := C.GoString(sortType) + goSort := C.GoString(sort) + goPriceMin := float32(priceMin) + goPriceMax := float32(priceMax) + goPageNum := int(pageNum) + goReturnNum := int(returnNum) + books, num, pNum, err := outGetGoodsListMsgByShopId(goShopId, goProxy, goIsImage, goSortType, goSort, goPriceMin, goPriceMax, goPageNum, goReturnNum) + // 构建统一格式的响应 + bookInfo := struct { + GoodsNum string `json:"goods_num,omitempty"` + PNum string `json:"pnum,omitempty"` + Data interface{} `json:"data,omitempty"` + }{ + GoodsNum: num, + PNum: pNum, + Data: books, + } + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: bookInfo, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取商品信息通过商品详情链接(带有0ut的都非官方标准接口) +// +//export OutGetGoodsMsgByDetailUrl +func OutGetGoodsMsgByDetailUrl(detailUrl, proxy *C.char) *C.char { + goDetailUrl := C.GoString(detailUrl) + goProxy := C.GoString(proxy) + response, err := outGetGoodsMsgByDetailUrl(goDetailUrl, goProxy) + // 构建统一格式的响应 + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: response, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +// 获取销量榜商品列表(带有Out的都非官放标准接口) +// +//export OutGetTopGoodsListMsg +func OutGetTopGoodsListMsg(catId C.int, proxy *C.char) *C.char { + goCatId := int(catId) + goProxy := C.GoString(proxy) + response, err := outGetTopGoodsListMsg(goCatId, goProxy) + // 构建统一格式的响应 + var apiResponse APIResponse + if err != nil { + apiResponse = APIResponse{ + Success: false, + Message: err.Error(), + } + } else { + apiResponse = APIResponse{ + Success: true, + Data: response, + } + } + // 转换为JSON字符串 + jsonData, marshalErr := json.Marshal(apiResponse) + if marshalErr != nil { + // 如果JSON序列化失败,返回错误信息 + apiResponse = APIResponse{ + Success: false, + Message: fmt.Sprintf("JSON序列化失败: %v", marshalErr), + } + errorJson, _ := json.Marshal(apiResponse) + return C.CString(string(errorJson)) + } + return C.CString(string(jsonData)) +} + +//export Initialize +func Initialize(configJSON *C.char) *C.char { + configStr := C.GoString(configJSON) + log.Printf("[DEBUG] 接收到的配置JSON: %s", configStr) + + var config Config + if err := json.Unmarshal([]byte(configStr), &config); err != nil { + return C.CString(fmt.Sprintf(`{"success":false,"message":"配置解析失败: %v"}`, err)) + } + initializeConfig(config) + + return C.CString(`{"success":true,"message":"初始化成功"}`) +} + +// 导出函数:释放C字符串内存 +// +//export FreeCString +func FreeCString(str *C.char) { + C.free(unsafe.Pointer(str)) +} + +//// 空main函数,编译DLL时需要 +//func main() { +// +//} diff --git a/md/main.md b/md/main.md new file mode 100644 index 0000000..dd79989 --- /dev/null +++ b/md/main.md @@ -0,0 +1,736 @@ +# 针对kongfz.dll文件使用教程 +## 1.需要先创建dll工具实例 + +### 1. 加载DLL,用于调用dll文件,代码示例: +```go +func main(){ + // 加载DLL + manager, err := NewDLLManager("main.dll") + if err != nil { + log.Fatalf("初始化DLL管理器失败: %v", err) + } + // 确保字段被释放,但是需要注意,如果加这个,可能把你的dll文件释放掉了,导致程序崩溃 + defer manager.Close() +} + + +type DLLManager struct { + dll *syscall.DLL +} + +func NewDLLManager(dllPath string) (*DLLManager, error) { + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载DLL失败: %v", err) + } + return &DLLManager{dll: dll}, nil +} + +func (m *DLLManager) Close() { + if m.dll != nil { + m.dll.Release() + } +} +func (m *DLLManager) Initialize(configJSON string) (string, error) { + return m.callFunction("Initialize", configJSON) +} + +// 修改后的 GetKFZGTImageURL - 需要5个参数 +func (m *DLLManager) GetKFZGTImageURL(proxyType, username, password, machineCode, isbn string) (string, error) { + return m.callFunctionFiveArgs("GetKFZGTImageURL", + proxyType, username, password, machineCode, isbn) +} + +// 修改后的 GetKFZSPTImageURL - 需要5个参数 +func (m *DLLManager) GetKFZSPTImageURL(proxyType, username, password, machineCode, isbn string) (string, error) { + return m.callFunctionFiveArgs("GetKFZSPTImageURL", + proxyType, username, password, machineCode, isbn) +} + +// 修改后的 GetKFZShopBookInfo - 需要12个参数 +func (m *DLLManager) GetKFZShopBookInfo(proxyType, username, password, machineCode, shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp string) (string, error) { + return m.callFunctionTwelveArgs("GetKFZShopBookInfo", + proxyType, username, password, machineCode, shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp) +} +``` + +### 2.初始化配置信息,代码示例: +````go +func main(){ + // 使用默认配置初始化 + config := createDefaultConfig() + configJSON, err := json.Marshal(config) + if err != nil { + log.Fatalf("序列化配置失败: %v", err) + } + + result, err := manager.Initialize(string(configJSON)) + if err != nil { + log.Fatalf("初始化失败: %v", err) + } + + var initResp APIResponses + if err := json.Unmarshal([]byte(result), &initResp); err != nil { + log.Fatalf("解析初始化响应失败: %v", err) + } + + if !initResp.Success { + log.Fatalf("初始化失败: %s", initResp.Message) + } +} +// 创建默认配置示例 +func createDefaultConfig() Configs { + var configs Configs + + // App配置 + configs.App.MaxRetryTimes = 3 + configs.App.RateLimitDelay = 1000 + configs.App.Size = 10 + configs.App.DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + // API配置 + configs.API.LoginURL = "https://login.kongfz.com/Pc/Login/account" + configs.API.BookSearchURL = "https://search.kongfz.com/pc-gw/search-web/client/pc/bookLib/keyword/list" + configs.API.ProductSearchURL = "https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list" + + // 代理配置(需要根据实际情况修改) + configs.Proxy.Servers = "http-dynamic.xiaoxiangdaili.com,http-dynamic-S02.xiaoxiangdaili.com,http-dynamic-S03.xiaoxiangdaili.com,http-dynamic-S04.xiaoxiangdaili.com" + configs.Proxy.Username = "1297757178467602432" + configs.Proxy.Password = "QgQBvP7f" + configs.Proxy.TailMachineCode = "b7bf22a237ec692f13fcc2c43ee63252" + configs.Proxy.TailCardKey = "DL_20_YK_1920acb2129844c2aabade3896560a9b" + configs.Proxy.ProxyFilePath = "dll/proxyConfig.dll" + + return configs +} + +// 配置结构 +type Configs struct { + App struct { + MaxRetryTimes int `json:"max_retry_times"` + RateLimitDelay int `json:"rate_limit_delay"` + Size int `json:"size"` + DefaultUserAgent string `json:"default_user_agent"` + } `json:"app"` + + API struct { + LoginURL string `json:"login_url"` + BookSearchURL string `json:"book_search_url"` + ProductSearchURL string `json:"product_search_url"` + } `json:"api"` + + Proxy struct { + Servers string `json:"servers"` + Username string `json:"username"` + Password string `json:"password"` + TailMachineCode string `json:"tail_machine_code"` + TailCardKey string `json:"tail_card_key"` + ProxyFilePath string `json:"proxy_file_path"` + } `json:"proxy"` +} +```` + +### 直接调用接口,代码示例:入参、返回参数都在下面 +```go +func main() { + // 调用方法示例 + shopResult, err := manager.GetKFZShopBookInfo( + proxyType, username, password, machineCode, + shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp, + ) +} +``` + +--- +## 接口详情 + +### 1. 获取孔网官图 + +**请求信息** + +gtResult, err := manager.GetKFZGTImageURL(testCase.isbn) + + +**请求参数** + +| 参数名 | 类型 | 必填 | 说明 | +|------|--------|----|-----------| +| proxyType | string | 是 | 代理类型(小象代理:CALF_ELEPHANT_PROXY,内置代理:TAIL_PROXY) | +| username | string | 是 | 小象代理账号 | +| password | string | 是 | 小象代理密码 | +| machineCode | string | 是 | 内置代理机械码 | +| isbn | string | 是 | 图书的 ISBN 号 | + +**响应数据** + +| 字段名 | 类型 | 说明 | 示例值 | +|--------|------|------|---------| +| `success` | boolean | 请求是否成功 | `true` | +| `message` | string | 响应消息 | `"查询成功"` | +| `data` | array | 图书信息列表 | - | + +**图书信息字段说明(data数据)** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `author` | string | 作者 | +| `binding_layout` | string | 装帧布局 | +| `book_name` | string | 图书名称 | +| `book_pic` | string | 图书图片(大图) | +| `book_pic_s` | string | 图书图片(小图) | +| `buyCount` | string | 购买数量 | +| `category` | string | 分类 | +| `condition` | string | 图书状况 | +| `content` | string | 内容简介 | +| `edition` | string | 版次 | +| `editor` | string | 编者 | +| `express_delivery_fee` | string | 快递费用 | +| `fix_price` | string | 定价 | +| `format` | string | 开本 | +| `isbn` | string | ISBN号 | +| `item_id` | number | 商品ID | +| `languages` | string | 语言 | +| `mid` | number | 商家ID | +| `pages` | string | 页数 | +| `paper` | string | 纸张 | +| `print_time` | string | 印刷时间 | +| `publication_time` | number | 出版时间(时间戳) | +| `publisher` | string | 出版社 | +| `sellCount` | string | 销售数量 | +| `shop_id` | number | 店铺ID | +| `wordage` | string | 字数 | + +**响应示例** + +```json +{ + "success": true, + "message": "查询成功", + "data": { + "author": "", + "binding_layout": "", + "book_name": "", + "book_pic": "", + "book_pic_s": "", + "buyCount": "", + "category": "", + "condition": "", + "content": "", + "edition": "", + "editor": "", + "express_delivery_fee": "", + "fix_price": "", + "format": "", + "isbn": "", + "item_id": 0, + "languages": "", + "mid": 0, + "pages": "", + "paper": "", + "print_time": "", + "publication_time": 0, + "publisher": "", + "sellCount": "", + "shop_id": 0, + "wordage": "" + } +} +``` + +### 2. 获取孔网实拍图 + +**请求信息** + +sptResult, err := manager.GetKFZSPTImageURL(testCase.isbn) + +**请求参数** + +| 参数名 | 类型 | 必填 | 说明 | +|------|--------|----|------------| +| proxyType | string | 是 | 代理类型(小象代理:CALF_ELEPHANT_PROXY,内置代理:TAIL_PROXY) | +| username | string | 是 | 小象代理账号 | +| password | string | 是 | 小象代理密码 | +| machineCode | string | 是 | 内置代理机械码 | +| isbn | string | 是 | 图书的 ISBN 号 | + +**响应数据** + +| 字段名 | 类型 | 说明 | 示例值 | +|--------|------|------|---------| +| `success` | boolean | 请求是否成功 | `true` | +| `message` | string | 响应消息 | `"查询成功"` | +| `data` | array | 图书信息列表 | - | + +**图书信息字段说明(data数据)** + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| `author` | string | 作者 | +| `binding_layout` | string | 装帧布局 | +| `book_name` | string | 图书名称 | +| `book_pic` | string | 图书图片(大图) | +| `book_pic_s` | string | 图书图片(小图) | +| `buyCount` | string | 购买数量 | +| `category` | string | 分类 | +| `condition` | string | 图书状况 | +| `content` | string | 内容简介 | +| `edition` | string | 版次 | +| `editor` | string | 编者 | +| `express_delivery_fee` | string | 快递费用 | +| `fix_price` | string | 定价 | +| `format` | string | 开本 | +| `isbn` | string | ISBN号 | +| `item_id` | number | 商品ID | +| `languages` | string | 语言 | +| `mid` | number | 商家ID | +| `pages` | string | 页数 | +| `paper` | string | 纸张 | +| `print_time` | string | 印刷时间 | +| `publication_time` | number | 出版时间(时间戳) | +| `publisher` | string | 出版社 | +| `sellCount` | string | 销售数量 | +| `shop_id` | number | 店铺ID | +| `wordage` | string | 字数 | + +**响应示例** + +```json +{ + "success": true, + "message": "查询成功", + "data": { + "author": "", + "binding_layout": "", + "book_name": "", + "book_pic": "", + "book_pic_s": "https://www0.kfzimg.com/sw/kfz-cos/kfzimg/adaaecbf/a1e5df0a21cfdbba_s.jpg", + "buyCount": "", + "category": "", + "condition": "", + "content": "", + "edition": "", + "editor": "", + "express_delivery_fee": "", + "fix_price": "", + "format": "", + "isbn": "", + "item_id": 0, + "languages": "", + "mid": 0, + "pages": "", + "paper": "", + "print_time": "", + "publication_time": 0, + "publisher": "", + "sellCount": "", + "shop_id": 0, + "wordage": "" + } +} +``` + +### 3. 获取店铺图书信息 + +**请求信息** + +shopResult, err := manager.GetKFZShopBookInfo( + proxyType, //代理类型 + username, //小象代理账号 + password, //小象代理密码 + machineCode, //内置代理机械码 + shopId, + isImage, + bookNum, + pageNum, + sortType, + sort, + priceDown, + priceUp, +) + +**请求参数** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|--------|----|------------------------------------------------| +| fetchMode | string | 是 | 是否使用代理 (使用代理:proxy 不使用代理:direct) | +| proxyType | string | 是 | 代理类型(小象代理:CALF_ELEPHANT_PROXY,内置代理:TAIL_PROXY) | +| username | string | 是 | 小象代理账号 | +| password | string | 是 | 小象代理密码 | +| machineCode | string | 是 | 内置代理机械码 | +| shopId | string | 是 | 店铺编号 | +| isImage | string | 否 | 是否有图片 | +| bookNum | string | 否 | 图书数量 | +| pageNum | string | 否 | 页数 | +| sortType | string | 否 | 排序类型(sort,putDate,newItem,price只有这四种) | +| sort | string | 否 | 排序方式(asc,desc) | +| priceUp | string | 否 | 最低价格 | +| priceDown | string | 否 | 最高价格 | + + +**响应数据** + +| 字段名 | 类型 | 说明 | 示例值 | +|--------|------|--------|---------| +| `success` | boolean | 请求是否成功 | `true` | +| `message` | string | 响应消息 | `"查询成功"` | +| `goods_num` | string | 商品数量 | `"查询成功"` | +| `pnum` | string | 店铺商品页数 | `"查询成功"` | +| `data` | array | 图书信息列表 | - | + +**图书信息字段说明(data数据)** + +| 字段名 | 类型 | 说明 | +|--------|------|--------| +| `book_name` | string | 书名 | +| `author` | string | 作者 | +| `publisher` | string | 出版社 | +| `isbn` | string | ISBN号 | +| `publication_time` | number | 出版时间(时间戳) | +| `edition` | string | 版次 | +| `print_time` | string | 印刷时间 | +| `fix_price` | string | 定价 | +| `binding_layout` | string | 装帧 | +| `format` | string | 开本 | +| `paper` | string | 纸张 | +| `pages` | string | 页数 | +| `wordage` | string | 字数 | +| `languages` | string | 语种 | +| `era` | string | 年代 | +| `engraving_method` | string | 刻印方式 | +| `dimensions` | string | 尺寸 | +| `volume_number` | string | 册数 | +| `book_pic` | string | 图书图片(官图) | +| `book_pic_s` | string | 图书图片(实拍图) | +| `selling_price` | string | 售价 | +| `condition` | string | 品相 | +| `express_delivery_fee` | string | 快递费 | +| `editor` | string | 编辑 | +| `category` | string | 分类 | +| `buy_count` | string | 买过 | +| `sell_count` | string | 买过 | +| `content` | string | 内容 | +| `mid` | number | 商家ID | +| `item_id` | number | 商品ID | +| `shop_id` | number | 店铺ID | + +**响应示例** + +```json +{ + "success": true, + "message": "查询成功", + "goods_num": "", + "pnum": "", + "data": [ + { + "author": "", + "binding_layout": "", + "book_name": "", + "book_pic": "", + "book_pic_s": "https://www0.kfzimg.com/sw/kfz-cos/kfzimg/cddbcbda/6833c1d27437d121_s.jpg", + "buyCount": "", + "category": "", + "condition": "", + "content": "", + "edition": "", + "editor": "", + "express_delivery_fee": "", + "fix_price": "", + "format": "", + "isbn": "", + "item_id": 0, + "languages": "", + "mid": 0, + "pages": "", + "paper": "", + "print_time": "", + "publication_time": 0, + "publisher": "", + "sellCount": "", + "shop_id": 0, + "wordage": "" + } + ] +} +``` + +### 4. 根据url获取单个图书详情信息 + +**请求信息** + +shopResult, err := manager.GetUrlBookDetails( + fetchMode, + proxyType, + username, + password, + machineCode, + url +) + +**请求参数** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|--------|----|------------------------------------------------| +| fetchMode | string | 是 | 是否使用代理 (使用代理:proxy 不使用代理:direct) | +| proxyType | string | 是 | 代理类型(小象代理:CALF_ELEPHANT_PROXY,内置代理:TAIL_PROXY) | +| username | string | 是 | 小象代理账号 | +| password | string | 是 | 小象代理密码 | +| machineCode | string | 是 | 内置代理机械码 | +| url | string | 是 | 传入详情页的url | + + +**响应数据** + +| 字段名 | 类型 | 说明 | 示例值 | +|--------|------|--------|---------| +| `success` | boolean | 请求是否成功 | `true` | +| `message` | string | 响应消息 | `"查询成功"` | +| `goods_num` | string | 商品数量 | `"查询成功"` | +| `pnum` | string | 店铺商品页数 | `"查询成功"` | +| `data` | array | 图书信息列表 | - | + +**图书信息字段说明(data数据)** + +| 字段名 | 类型 | 说明 | +|--------|------|--------| +| `book_name` | string | 书名 | +| `author` | string | 作者 | +| `publisher` | string | 出版社 | +| `isbn` | string | ISBN号 | +| `publication_time` | number | 出版时间(时间戳) | +| `edition` | string | 版次 | +| `print_time` | string | 印刷时间 | +| `fix_price` | string | 定价 | +| `binding_layout` | string | 装帧 | +| `format` | string | 开本 | +| `paper` | string | 纸张 | +| `pages` | string | 页数 | +| `wordage` | string | 字数 | +| `languages` | string | 语种 | +| `era` | string | 年代 | +| `engraving_method` | string | 刻印方式 | +| `dimensions` | string | 尺寸 | +| `volume_number` | string | 册数 | +| `book_pic` | string | 图书图片(官图) | +| `book_pic_s` | string | 图书图片(实拍图) | +| `selling_price` | string | 售价 | +| `condition` | string | 品相 | +| `express_delivery_fee` | string | 快递费 | +| `editor` | string | 编辑 | +| `category` | string | 分类 | +| `buy_count` | string | 买过 | +| `sell_count` | string | 买过 | +| `content` | string | 内容 | +| `mid` | number | 商家ID | +| `item_id` | number | 商品ID | +| `shop_id` | number | 店铺ID | + +**响应示例** + +```json +{ + "success": true, + "message": "查询成功", + "goods_num": "", + "pnum": "", + "data": [ + { + "author": "", + "binding_layout": "", + "book_name": "", + "book_pic": "", + "book_pic_s": "https://www0.kfzimg.com/sw/kfz-cos/kfzimg/cddbcbda/6833c1d27437d121_s.jpg", + "buyCount": "", + "category": "", + "condition": "", + "content": "", + "edition": "", + "editor": "", + "express_delivery_fee": "", + "fix_price": "", + "format": "", + "isbn": "", + "item_id": 0, + "languages": "", + "mid": 0, + "pages": "", + "paper": "", + "print_time": "", + "publication_time": 0, + "publisher": "", + "sellCount": "", + "shop_id": 0, + "wordage": "" + } + ] +} +``` + +### 5. 初始化 + +**请求信息** + +manager.Initialize( + configJSON, +) + +**请求参数** + +| 参数名 | 类型 | 必填 | 说明 | +|--------|--------|----|------| +| configJSON | string | 是 | 配置信息 | + +**响应数据** + +| 字段名 | 类型 | 说明 | 示例值 | +|--------|------|--------|---------| +| `success` | boolean | 请求是否成功 | `true` | +| `message` | string | 响应消息 | `"查询成功"` | + +**图书信息字段说明(data数据)** + +| 字段名 | 类型 | 说明 | +|--------|------|--------| +| `book_name` | string | 书名 | +| `author` | string | 作者 | +| `publisher` | string | 出版社 | +| `isbn` | string | ISBN号 | +| `publication_time` | number | 出版时间(时间戳) | +| `edition` | string | 版次 | +| `print_time` | string | 印刷时间 | +| `fix_price` | string | 定价 | +| `binding_layout` | string | 装帧 | +| `format` | string | 开本 | +| `paper` | string | 纸张 | +| `pages` | string | 页数 | +| `wordage` | string | 字数 | +| `languages` | string | 语种 | +| `era` | string | 年代 | +| `engraving_method` | string | 刻印方式 | +| `dimensions` | string | 尺寸 | +| `volume_number` | string | 册数 | +| `book_pic` | string | 图书图片(官图) | +| `book_pic_s` | string | 图书图片(实拍图) | +| `selling_price` | string | 售价 | +| `condition` | string | 品相 | +| `express_delivery_fee` | string | 快递费 | +| `editor` | string | 编辑 | +| `category` | string | 分类 | +| `buy_count` | string | 买过 | +| `sell_count` | string | 买过 | +| `content` | string | 内容 | +| `mid` | number | 商家ID | +| `item_id` | number | 商品ID | +| `shop_id` | number | 店铺ID | + + + + +# proxyConfig.dll文件使用讲解 +由于是kongfz.dll文件去调用proxyConfig.dll文件里面的函数,无需再去调用。 +除非直接使用proxyConfig.dll文件。 +## 1.直接使用proxyConfig.dll文件,应先加载DLL +```go +func main(){ + // 加载DLL + manager, err := NewDLLManager("文件路径") + if err != nil { + log.Fatalf("初始化DLL管理器失败: %v", err) + } + // 确保字段被释放,但是需要注意,如果加这个,可能把你的dll文件释放掉了,导致程序崩溃 + defer manager.Close() +} + + +type DLLManager struct { + dll *syscall.DLL +} + +func NewDLLManager(dllPath string) (*DLLManager, error) { + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载DLL失败: %v", err) + } + return &DLLManager{dll: dll}, nil +} + +func (m *DLLManager) Close() { + if m.dll != nil { + m.dll.Release() + } +} + +// ProxyTypeManager 调用代理类型管理器 +func (m *ProxyConfigManager) ProxyTypeManager(proxyType, username, password, machineCode string) (string, error) { + proc, err := m.dll.FindProc("ProxyTypeManager") + if err != nil { + return "", fmt.Errorf("找不到函数 ProxyTypeManager: %v", err) + } + + // 准备参数 + proxyTypePtr, _ := syscall.BytePtrFromString(proxyType) + usernamePtr, _ := syscall.BytePtrFromString(username) + passwordPtr, _ := syscall.BytePtrFromString(password) + machineCodePtr, _ := syscall.BytePtrFromString(machineCode) + + // 调用函数 + r1, _, err := proc.Call( + uintptr(unsafe.Pointer(proxyTypePtr)), + uintptr(unsafe.Pointer(usernamePtr)), + uintptr(unsafe.Pointer(passwordPtr)), + uintptr(unsafe.Pointer(machineCodePtr)), + ) + + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用 ProxyTypeManager 失败: %v", err) + } + + // 转换结果 + result := (*byte)(unsafe.Pointer(r1)) + var resultBytes []byte + for i := 0; ; i++ { + bytePtr := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) + if *bytePtr == 0 { + break + } + resultBytes = append(resultBytes, *bytePtr) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeCString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} +``` + +## 直接调用ProxyTypeManager函数方法 +```go +typeManager, err := globalProxyManager.ProxyTypeManager( + proxyType, + username, + password, + machineCode, +) +``` +**请求信息** +typeManager, err := globalProxyManager.ProxyTypeManager( + proxyType, + username, + password, + machineCode, +) + +**请求参数** + +| 参数名 | 类型 | 必填 | 说明 | +|------|--------|----|------------| +| proxyType | string | 是 | 代理类型(小象代理:CALF_ELEPHANT_PROXY,内置代理:TAIL_PROXY) | +| username | string | 是 | 小象代理账号 | +| password | string | 是 | 小象代理密码 | +| machineCode | string | 是 | 内置代理机械码 | + +**响应信息** +string字符串: http://18434270290:JWt15lWW@111.179.77.244:44751 \ No newline at end of file diff --git a/proxyConfig.go b/proxyConfig.go new file mode 100644 index 0000000..6fb0ff8 --- /dev/null +++ b/proxyConfig.go @@ -0,0 +1,510 @@ +package main + +// #include +import "C" +import ( + "crypto/md5" + "encoding/json" + "fmt" + "github.com/parnurzeal/gorequest" + "log" + "math/rand" + "net/url" + "strings" + "sync" + "time" +) + +// 代理类型常量 +const ( + CalfElephantProxyType = "CALF_ELEPHANT_PROXY" + TailProxyType = "TAIL_PROXY" +) + +// 代理服务器列表 +var ( + servers = []string{ + "http-dynamic.xiaoxiangdaili.com", + "http-dynamic-S02.xiaoxiangdaili.com", + "http-dynamic-S03.xiaoxiangdaili.com", + "http-dynamic-S04.xiaoxiangdaili.com", + } + randMutex sync.Mutex + globalRand *rand.Rand + + // 代理健康状态管理 + proxyHealthMaps = make(map[string]*ProxyHealth) + proxyHealthMutex sync.RWMutex +) + +// ProxyManager 代理管理器结构体 +type ProxyManager struct { + servers []string `json:"servers"` // 代理服务器列表 + username string `json:"username"` // 代理账号 + password string `json:"password"` // 代理密码 + tailCardSecret string `json:"tail_card_secret"` // 尾巴代理卡密 + proxyType string `json:"proxy_type"` // 代理类型 CALF_ELEPHANT_PROXY/TAIL_PROXY +} + +// 代理健康状态 +type ProxyHealth struct { + SuccessCount int // 成功次数 + FailCount int // 失败次数 + LastCheck time.Time // 最后检查时间 + ResponseTime time.Duration // 响应时间 + IsHealthy bool // 是否健康 +} + +func init() { + // 创建全局的随机数生成器 + globalRand = rand.New(rand.NewSource(time.Now().UnixNano())) +} + +// 获取代理URL +func proxyTypeManager(proxyType, username, password, machineCode string) (string, error) { + switch proxyType { + case CalfElephantProxyType: + return buildCalfElephantProxyURL(username, password) + case TailProxyType: + return buildTailProxyURL(machineCode) + default: + return "", fmt.Errorf("不支持的代理类型: %s", proxyType) + } +} + +// 构建小象代理URL +func buildCalfElephantProxyURL(username, password string) (string, error) { + server := randomServer() + proxyURL := fmt.Sprintf("http://%s:%s@%s:%d", + url.QueryEscape(username), + url.QueryEscape(password), + 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 +} + +// 尝试下一个小象代理服务器 +func tryNextCalfElephantProxy(username, password, failedServer string) (string, error) { + // 创建服务器副本并排除失败的服务器 + availableServers := make([]string, 0) + for _, server := range servers { + if server != failedServer { + availableServers = append(availableServers, server) + } + } + + if len(availableServers) == 0 { + return "", fmt.Errorf("所有小象代理服务器都不可用") + } + + // 随机尝试可用服务器 + for _, server := range shuffleServers(availableServers) { + 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("未获取到有效代理") + } + + // 过滤并选择健康的代理 + healthyProxies := filterHealthyProxies(proxies) + if len(healthyProxies) > 0 { + proxyURL := "http://" + randomElement(healthyProxies) + log.Printf("[INFO] 使用健康尾巴代理: %s", proxyURL) + return proxyURL, nil + } + + // 如果没有健康代理,检测所有代理 + log.Printf("[INFO] 未找到健康代理,开始检测代理可用性...") + return findWorkingTailProxy(proxies) +} + +// 过滤健康代理 +func filterHealthyProxies(proxies []string) []string { + proxyHealthMutex.RLock() + defer proxyHealthMutex.RUnlock() + + var healthy []string + for _, proxy := range proxies { + if health, exists := proxyHealthMaps[proxy]; exists && health.IsHealthy { + // 检查是否在最近检查过(5分钟内) + if time.Since(health.LastCheck) < 5*time.Minute { + healthy = append(healthy, proxy) + } + } + } + return healthy +} + +// 查找可用的尾巴代理 +func findWorkingTailProxy(proxies []string) (string, error) { + // 打乱代理顺序 + shuffledProxies := shuffleServers(proxies) + + // 并发检测代理 + type proxyResult struct { + proxy string + err error + } + + ch := make(chan proxyResult, len(shuffledProxies)) + var wg sync.WaitGroup + + // 限制并发数 + sem := make(chan struct{}, 5) + + for _, proxy := range shuffledProxies { + wg.Add(1) + go func(p string) { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + + proxyURL := "http://" + p + err := checkProxyHealth(proxyURL) + ch <- proxyResult{proxy: p, err: err} + }(proxy) + } + + wg.Wait() + 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() + + req := gorequest.New().Proxy(proxyURL).Timeout(10 * time.Second) + resp, _, errs := req.Get("https://shop.kongfz.com/").End() + + responseTime := time.Since(start) + + if len(errs) > 0 { + updateProxyHealth(proxyURL, false, responseTime) + return fmt.Errorf("代理连接失败: %v", errs) + } + + 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 +} + +// 更新代理健康状态 +func updateProxyHealth(proxyURL string, success bool, responseTime time.Duration) { + proxyHealthMutex.Lock() + defer proxyHealthMutex.Unlock() + + host := getProxyHost(proxyURL) + if _, exists := proxyHealthMaps[host]; !exists { + proxyHealthMaps[host] = &ProxyHealth{} + } + + health := proxyHealthMaps[host] + health.LastCheck = time.Now() + + if success { + health.SuccessCount++ + health.FailCount = 0 + health.ResponseTime = responseTime + health.IsHealthy = true + } else { + health.FailCount++ + health.SuccessCount = 0 + // 连续失败3次标记为不健康 + if health.FailCount >= 3 { + health.IsHealthy = false + } + } +} + +// 获取代理主机名 +func getProxyHost(proxyURL string) string { + if strings.HasPrefix(proxyURL, "http://") { + proxyURL = proxyURL[7:] + } + // 去除认证信息 + if atIndex := strings.Index(proxyURL, "@"); atIndex != -1 { + proxyURL = proxyURL[atIndex+1:] + } + // 去除端口 + if colonIndex := strings.Index(proxyURL, ":"); colonIndex != -1 { + proxyURL = proxyURL[:colonIndex] + } + return proxyURL +} + +// 随机代理服务器 +func randomServer() string { + return randomElement(servers) +} + +// 线程安全的随机元素选择 +func randomElement(slice []string) string { + 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) + globalRand.Shuffle(len(shuffled), func(i, j int) { + shuffled[i], shuffled[j] = shuffled[j], shuffled[i] + }) + return shuffled +} + +// 检查卡密是否过期 +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"` + Data struct { + MachineCode string `json:"machine_code"` + IpExpTime string `json:"ip_exp_time"` + IpThread int `json:"ip_thread"` + IpCardCode string `json:"ip_card_code"` + } `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, + } + _, 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 + } + resp.Data.MachineCode = machineCode + return &resp, nil + } else if resp.Code == 200 { + return &resp, nil + } else { + return nil, fmt.Errorf("查询机器码失败: %s", resp.Message) + } +} + +// 充值卡密 +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, + } + _, 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"` // 返回消息 + Data struct { + IpExpTime string `json:"ip_exp_time"` // 过期时间 + IpThread int `json:"ip_thread"` // 线程 + MachineCode string `json:"machine_code"` // 机器码 + } `json:"data"` + } + if err := json.Unmarshal([]byte(body), &resp); err != nil { + return "", fmt.Errorf("解析响应失败: %v", err) + } + if resp.Code != 200 { + return "", fmt.Errorf("充值卡密失败: %s", resp.Message) + } + return resp.Data.MachineCode, nil +} + +// 获取代理服务器列表 +func getProxies(machineCode string) ([]string, error) { + log.Printf("[INFO] 开始获取代理列表: %s", machineCode) + // 生成签名 + sign := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("9999%s9999", machineCode)))) + + GetProxiesUrl := fmt.Sprintf("http://114.66.2.223:7842/api/proxies/get_proxy?machine_code=%s&sign=%s&agent_id=9999", + machineCode, sign) + + req := gorequest.New().Get(GetProxiesUrl).Timeout(20 * time.Second) + _, body, errs := req.End() + if len(errs) > 0 { + return nil, fmt.Errorf("获取代理失败: %v", errs) + } + + // 检查是否为JSON错误响应 + if strings.HasPrefix(strings.TrimSpace(body), "{") && strings.HasSuffix(strings.TrimSpace(body), "}") { + // 尝试解析为JSON错误响应 + var errorResp struct { + Code int `json:"code"` + Message string `json:"message"` + } + if err := json.Unmarshal([]byte(body), &errorResp); err == nil { + return nil, fmt.Errorf("获取代理失败: %s (错误码: %d)", errorResp.Message, errorResp.Code) + } + } + + // 解析响应 + lines := strings.Split(strings.TrimSpace(body), "\n") + var proxies []string + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" && !strings.HasPrefix(line, "{") { + proxies = append(proxies, line) + } + } + if len(proxies) == 0 { + return nil, fmt.Errorf("未获取到有效代理") + } + + log.Printf("[INFO] 获取到 %d 个代理", len(proxies)) + return proxies, nil +} + +// 初始化代理信息 +func initProxy() { + +} + +// 导出函数:获取代理健康状态(用于调试) +// +//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{}{ + "success_count": health.SuccessCount, + "fail_count": health.FailCount, + "last_check": health.LastCheck.Format(time.RFC3339), + "response_time": health.ResponseTime.String(), + "is_healthy": health.IsHealthy, + } + } + + jsonData, err := json.Marshal(healthInfo) + if err != nil { + return C.CString(fmt.Sprintf(`{"error": "序列化健康信息失败: %v"}`, err)) + } + + return C.CString(string(jsonData)) +} + +// 导出函数:代理类型管理器 +// +//export ProxyTypeManager +func ProxyTypeManager(proxyType, username, password, machineCode *C.char) *C.char { + 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) + log.Printf("[ERROR] 代理类型管理器错误: %v", err) + return C.CString(errorMsg) + } + + log.Printf("[DEBUG] 代理类型管理器返回: %s", proxyURL) + return C.CString(proxyURL) +} + +//// 导出函数:释放C字符串内存 +//// +////export FreeCString +//func FreeCString(str *C.char) { +// C.free(unsafe.Pointer(str)) +//} +// +//func main() { +// +//} diff --git a/zjdydll.go b/zjdydll.go new file mode 100644 index 0000000..2d39be0 --- /dev/null +++ b/zjdydll.go @@ -0,0 +1,1009 @@ +package main + +import "C" +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "syscall" + "unsafe" +) + +// 配置结构 +type Configs struct { + App struct { + MaxRetryTimes int `json:"max_retry_times"` + RateLimitDelay int `json:"rate_limit_delay"` + Size int `json:"size"` + DefaultUserAgent string `json:"default_user_agent"` + } `json:"app"` + + API struct { + LoginURL string `json:"login_url"` + BookSearchURL string `json:"book_search_url"` + ProductSearchURL string `json:"product_search_url"` + } `json:"api"` + + Proxy struct { + Servers string `json:"servers"` + Username string `json:"username"` + Password string `json:"password"` + TailMachineCode string `json:"tail_machine_code"` + TailCardKey string `json:"tail_card_key"` + ProxyFilePath string `json:"proxy_file_path"` + } `json:"proxy"` +} + +// API响应结构 +type APIResponses struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` + GoodsNum string `json:"goods_num,omitempty"` + PNum string `json:"pnum,omitempty"` + Data interface{} `json:"data,omitempty"` + Error string `json:"error,omitempty"` +} + +type DLLManager struct { + dll *syscall.DLL +} + +func NewDLLManager(dllPath string) (*DLLManager, error) { + dll, err := syscall.LoadDLL(dllPath) + if err != nil { + return nil, fmt.Errorf("加载DLL失败: %v", err) + } + + return &DLLManager{dll: dll}, nil +} + +func (m *DLLManager) Close() { + if m.dll != nil { + m.dll.Release() + } +} + +// 单参数函数调用 +func (m *DLLManager) callFunction(funcName string, arg string) (string, error) { + proc, err := m.dll.FindProc(funcName) + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", funcName, err) + } + + argPtr, _ := syscall.BytePtrFromString(arg) + r1, _, err := proc.Call(uintptr(unsafe.Pointer(argPtr))) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 %s 失败: %v", funcName, err) + } + + result := (*byte)(unsafe.Pointer(r1)) + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i)))) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} + +// 二参数函数调用 +func (m *DLLManager) callFunctionTwoArgs(funcName string, args ...string) (string, error) { + if len(args) != 2 { + return "", fmt.Errorf("函数 %s 需要2个参数,但提供了 %d 个", funcName, len(args)) + } + + proc, err := m.dll.FindProc(funcName) + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", funcName, err) + } + + // 准备参数指针 + argPtrs := make([]uintptr, 2) + for i, arg := range args { + argPtr, _ := syscall.BytePtrFromString(arg) + argPtrs[i] = uintptr(unsafe.Pointer(argPtr)) + } + + r1, _, err := proc.Call( + argPtrs[0], // proxyType + argPtrs[1], // username + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 %s 失败: %v", funcName, err) + } + + result := (*byte)(unsafe.Pointer(r1)) + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i)))) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} + +// 五参数函数调用 - 用于 GetKFZGTImageURL 和 GetKFZSPTImageURL +func (m *DLLManager) callFunctionFiveArgs(funcName string, args ...string) (string, error) { + if len(args) != 5 { + return "", fmt.Errorf("函数 %s 需要5个参数,但提供了 %d 个", funcName, len(args)) + } + + proc, err := m.dll.FindProc(funcName) + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", funcName, err) + } + + // 准备参数指针 + argPtrs := make([]uintptr, 5) + for i, arg := range args { + argPtr, _ := syscall.BytePtrFromString(arg) + argPtrs[i] = uintptr(unsafe.Pointer(argPtr)) + } + + r1, _, err := proc.Call( + argPtrs[0], // proxyType + argPtrs[1], // username + argPtrs[2], // password + argPtrs[3], // machineCode + argPtrs[4], // isbn + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 %s 失败: %v", funcName, err) + } + + result := (*byte)(unsafe.Pointer(r1)) + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i)))) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} + +// 六参数函数调用 +func (m *DLLManager) callFunctionSixArgs(funcName string, args ...string) (string, error) { + if len(args) != 6 { + return "", fmt.Errorf("函数 %s 需要6个参数,但提供了 %d 个", funcName, len(args)) + } + + proc, err := m.dll.FindProc(funcName) + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", funcName, err) + } + + // 准备参数指针 + argPtrs := make([]uintptr, 6) + for i, arg := range args { + argPtr, _ := syscall.BytePtrFromString(arg) + argPtrs[i] = uintptr(unsafe.Pointer(argPtr)) + } + + r1, _, err := proc.Call( + argPtrs[0], // fetchMode + argPtrs[1], // proxyType + argPtrs[2], // username + argPtrs[3], // password + argPtrs[4], // machineCode + argPtrs[5], // isbn + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 %s 失败: %v", funcName, err) + } + + result := (*byte)(unsafe.Pointer(r1)) + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i)))) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} + +// 十二参数函数调用 - 专门用于 GetKFZShopBookInfo +func (m *DLLManager) callFunctionTwelveArgs(funcName string, args ...string) (string, error) { + if len(args) != 12 { + return "", fmt.Errorf("函数 %s 需要12个参数,但提供了 %d 个", funcName, len(args)) + } + + proc, err := m.dll.FindProc(funcName) + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", funcName, err) + } + + // 准备参数指针 + argPtrs := make([]uintptr, 12) + for i, arg := range args { + argPtr, _ := syscall.BytePtrFromString(arg) + argPtrs[i] = uintptr(unsafe.Pointer(argPtr)) + } + + r1, _, err := proc.Call( + argPtrs[0], // proxyType + argPtrs[1], // username + argPtrs[2], // password + argPtrs[3], // machineCode + argPtrs[4], // shopId + argPtrs[5], // isImage + argPtrs[6], // bookNum + argPtrs[7], // pageNum + argPtrs[8], // sortType + argPtrs[9], // sort + argPtrs[10], // priceDown + argPtrs[11], // priceUp + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 %s 失败: %v", funcName, err) + } + + result := (*byte)(unsafe.Pointer(r1)) + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i)))) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} + +// 十三参数函数调用 - 专门用于 GetKFZShopBookInfo +func (m *DLLManager) callFunctionThirteenArgs(funcName string, args ...string) (string, error) { + if len(args) != 13 { + return "", fmt.Errorf("函数 %s 需要13个参数,但提供了 %d 个", funcName, len(args)) + } + + proc, err := m.dll.FindProc(funcName) + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", funcName, err) + } + + // 准备参数指针 + argPtrs := make([]uintptr, 13) + for i, arg := range args { + argPtr, _ := syscall.BytePtrFromString(arg) + argPtrs[i] = uintptr(unsafe.Pointer(argPtr)) + } + + r1, _, err := proc.Call( + argPtrs[0], // fetchMode + argPtrs[1], // proxyType + argPtrs[2], // username + argPtrs[3], // password + argPtrs[4], // machineCode + argPtrs[5], // shopId + argPtrs[6], // isImage + argPtrs[7], // bookNum + argPtrs[8], // pageNum + argPtrs[9], // sortType + argPtrs[10], // sort + argPtrs[11], // priceDown + argPtrs[12], // priceUp + ) + if err != nil && err.Error() != "The operation completed successfully." { + return "", fmt.Errorf("调用函数 %s 失败: %v", funcName, err) + } + + result := (*byte)(unsafe.Pointer(r1)) + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i)))) + } + + // 释放内存 + freeProc, _ := m.dll.FindProc("FreeString") + if freeProc != nil { + freeProc.Call(r1) + } + + return string(resultBytes), nil +} + +func (m *DLLManager) Initialize(configJSON string) (string, error) { + return m.callFunction("Initialize", configJSON) +} + +// 修改后的 GetKFZGTImageURL - 需要5个参数 +func (m *DLLManager) GetKFZGTImageURL(proxyType, username, password, machineCode, isbn string) (string, error) { + return m.callFunctionFiveArgs("GetKFZGTImageURL", + proxyType, username, password, machineCode, isbn) +} + +// 修改后的 GetKFZSPTImageURL - 需要5个参数 +func (m *DLLManager) GetKFZSPTImageURL(proxyType, username, password, machineCode, isbn string) (string, error) { + return m.callFunctionFiveArgs("GetKFZSPTImageURL", + proxyType, username, password, machineCode, isbn) +} + +// 修改后的 GetKFZShopBookInfo - 需要12个参数 +func (m *DLLManager) GetKFZShopBookInfo(fetchMode, proxyType, username, password, machineCode, shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp string) (string, error) { + return m.callFunctionThirteenArgs("GetKFZShopBookInfo", + fetchMode, proxyType, username, password, machineCode, shopId, isImage, bookNum, pageNum, sortType, sort, priceDown, priceUp) +} + +// 获取 GetUrlBookDetails - 需要6个参数 +func (m *DLLManager) GetUrlBookDetails(fetchMode, proxyType, username, password, machineCode, url string) (string, error) { + return m.callFunctionSixArgs("GetUrlBookDetails", fetchMode, proxyType, username, password, machineCode, url) +} + +// 获取 OutLogin +func (m *DLLManager) OutLogin(username, password string) (string, error) { + // 获取函数 + proc, err := m.dll.FindProc("OutLogin") + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", "OutLogin", err) + } + // 转换参数 + usernamePtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(username))) + passwordPtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(password))) + // 调用函数 + ret, _, err := proc.Call(usernamePtr, passwordPtr) + if ret == 0 { + return "", fmt.Errorf("DLL调用失败: %v", err) + } + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i)))) + } + + // 释放内存(假设DLL提供了FreeMemory函数) + findProc, err := m.dll.FindProc("FreeString") + if findProc != nil { + findProc.Call(ret) + } + return string(resultBytes), nil +} + +// 获取 OutGetImageByIsbn +func (m *DLLManager) OutGetImageByIsbn(token string, isbn string, proxy string, isLiveImage bool, isReturnMsg bool) (string, error) { + // 获取函数 + proc, err := m.dll.FindProc("OutGetImageByIsbn") + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", "OutGetImageByIsbn", err) + } + // 转换参数 + tokenPtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(token))) + isbnPtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(isbn))) + var proxyPtr uintptr + if proxy != "" { + proxyPtr = uintptr(unsafe.Pointer(syscall.StringBytePtr(proxy))) + } else { + proxyPtr = 0 + } + var isLiveImageInt int + if isLiveImage { + isLiveImageInt = 0 + } else { + isLiveImageInt = 1 + } + isLiveImagePtr := uintptr(isLiveImageInt) + var isReturnMsgInt int + if isReturnMsg { + isLiveImageInt = 0 + } else { + isLiveImageInt = 1 + } + isReturnMsgPtr := uintptr(isReturnMsgInt) + // 调用函数 + ret, _, err := proc.Call(tokenPtr, isbnPtr, proxyPtr, isLiveImagePtr, isReturnMsgPtr) + if ret == 0 { + return "", fmt.Errorf("DLL调用失败: %v", err) + } + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i)))) + } + + // 释放内存(假设DLL提供了FreeMemory函数) + findProc, err := m.dll.FindProc("FreeString") + if findProc != nil { + findProc.Call(ret) + } + return string(resultBytes), nil +} + +// 获取 OutGetGoodsMsgByDetailUrl +func (m *DLLManager) OutGetGoodsMsgByDetailUrl(url, proxy string) (string, error) { + // 获取函数 + proc, err := m.dll.FindProc("OutGetGoodsMsgByDetailUrl") + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", "OutGetGoodsMsgByDetailUrl", err) + } + // 转换参数 + urlPtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(url))) + var proxyPtr uintptr + if proxy != "" { + proxyPtr = uintptr(unsafe.Pointer(syscall.StringBytePtr(proxy))) + } else { + proxyPtr = 0 + } + // 调用函数 + ret, _, err := proc.Call(urlPtr, proxyPtr) + if ret == 0 { + return "", fmt.Errorf("DLL调用失败: %v", err) + } + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i)))) + } + + // 释放内存(假设DLL提供了FreeMemory函数) + findProc, err := m.dll.FindProc("FreeString") + if findProc != nil { + findProc.Call(ret) + } + return string(resultBytes), nil +} + +// 获取 OutGetGoodsListMsgByShopId +func (m *DLLManager) OutGetGoodsListMsgByShopId(shopId int, proxy string, isImage bool, sortType string, sort string, priceMin float32, priceMax float32, pageNum, returnNum int) (string, error) { + // 获取函数 + proc, err := m.dll.FindProc("OutGetGoodsListMsgByShopId") + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", "OutGetGoodsListMsgByShopId", err) + } + // 转换参数 + shopIdPtr := uintptr(shopId) + var isImageInt int + if isImage { + isImageInt = 0 + } else { + isImageInt = 1 + } + isImagePtr := uintptr(isImageInt) + sortTypePtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(sortType))) + sortPtr := uintptr(unsafe.Pointer(syscall.StringBytePtr(sort))) + priceMinPtr := uintptr(*(*uint32)(unsafe.Pointer(&priceMin))) + priceMaxPtr := uintptr(*(*uint32)(unsafe.Pointer(&priceMax))) + pageNumPtr := uintptr(pageNum) + returnNumPtr := uintptr(returnNum) + var proxyPtr uintptr + if proxy != "" { + proxyPtr = uintptr(unsafe.Pointer(syscall.StringBytePtr(proxy))) + } else { + proxyPtr = 0 + } + // 调用函数 + ret, _, err := proc.Call(shopIdPtr, proxyPtr, isImagePtr, sortTypePtr, sortPtr, priceMinPtr, priceMaxPtr, pageNumPtr, returnNumPtr) + if ret == 0 { + return "", fmt.Errorf("DLL调用失败: %v", err) + } + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i)))) + } + + // 释放内存(假设DLL提供了FreeMemory函数) + findProc, err := m.dll.FindProc("FreeString") + if findProc != nil { + findProc.Call(ret) + } + return string(resultBytes), nil +} + +// 获取 OutGetTopGoodsListMsg +func (m *DLLManager) OutGetTopGoodsListMsg(catId int, proxy string) (string, error) { + // 获取函数 + proc, err := m.dll.FindProc("OutGetTopGoodsListMsg") + if err != nil { + return "", fmt.Errorf("找不到函数 %s: %v", "OutGetTopGoodsListMsg", err) + } + // 转换参数 + catIdPtr := uintptr(catId) + var proxyPtr uintptr + if proxy != "" { + proxyPtr = uintptr(unsafe.Pointer(syscall.StringBytePtr(proxy))) + } else { + proxyPtr = 0 + } + // 调用函数 + ret, _, err := proc.Call(catIdPtr, proxyPtr) + if ret == 0 { + return "", fmt.Errorf("DLL调用失败: %v", err) + } + + // 将C字符串转换为Go字符串 + var resultBytes []byte + for i := 0; ; i++ { + if *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i))) == 0 { + break + } + resultBytes = append(resultBytes, *(*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ret)) + uintptr(i)))) + } + + // 释放内存(假设DLL提供了FreeMemory函数) + findProc, err := m.dll.FindProc("FreeString") + if findProc != nil { + findProc.Call(ret) + } + return string(resultBytes), nil +} + +// 创建默认配置 +func createDefaultConfig() Config { + var configs Config + + // App配置 + configs.App.MaxRetryTimes = 3 + configs.App.RateLimitDelay = 1000 + configs.App.Size = 10 + configs.App.DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" + + // API配置 + configs.API.LoginURL = "https://login.kongfz.com/Pc/Login/account" + configs.API.BookSearchURL = "https://search.kongfz.com/pc-gw/search-web/client/pc/bookLib/keyword/list" + configs.API.ProductSearchURL = "https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list" + + // 代理配置(需要根据实际情况修改) + configs.Proxy.Servers = "http-dynamic.xiaoxiangdaili.com,http-dynamic-S02.xiaoxiangdaili.com,http-dynamic-S03.xiaoxiangdaili.com,http-dynamic-S04.xiaoxiangdaili.com" + configs.Proxy.Username = "1297757178467602432" + configs.Proxy.Password = "QgQBvP7f" + configs.Proxy.TailMachineCode = "b7bf22a237ec692f13fcc2c43ee63252" + configs.Proxy.TailCardKey = "DL_20_YK_1920acb2129844c2aabade3896560a9b" + configs.Proxy.ProxyFilePath = "dll/proxyConfig.dll" + + configs.Database.Username = "newAdmin" + configs.Database.Password = "bYPp8SbBe5F7nz2i" + configs.Database.Host = "146.56.227.42:3306" + configs.Database.Name = "newadmin" + + return configs +} + +// 获取当前可执行文件所在目录 +func getExecutableDir() string { + exePath, err := os.Executable() + if err != nil { + return "." + } + return filepath.Dir(exePath) +} + +// 格式化输出JSON响应 +func printFormattedResponse(operation string, result string, err error) int { + var apiResp APIResponses + fmt.Printf("\n%s:\n", operation) + fmt.Println("=" + string(make([]byte, 50)) + "=") + + if err != nil { + fmt.Printf("❌ 错误: %v\n", err) + } + var count int + if err := json.Unmarshal([]byte(result), &apiResp); err == nil { + if apiResp.Success { + fmt.Printf("✅ 成功: %s\n", apiResp.Message) + if apiResp.Data != nil { + fmt.Println("📊 数据:") + jsonData, _ := json.MarshalIndent(apiResp.Data, "", " ") + fmt.Println(string(jsonData)) + // 方法1: 判断 Data 的类型并统计条数 + switch data := apiResp.Data.(type) { + case []interface{}: + // 如果是切片/数组 + count = len(data) + fmt.Printf("📈 数据条数: %d\n", count) + case []BookInfo: + // 如果是 BookInfo 切片 + count = len(data) + fmt.Printf("📚 图书数量: %d\n", count) + case map[string]interface{}: + // 如果是映射 + count = len(data) + fmt.Printf("🗂️ 数据字段数: %d\n", count) + case []ProductInfo: + // 如果是 ProductInfo 切片 + count = len(data) + fmt.Printf("🛒 商品数量: %d\n", count) + default: + fmt.Printf("ℹ️ 数据类型: %T\n", apiResp.Data) + } + } + if apiResp.GoodsNum != "" { + fmt.Printf("📦 商品数量: %s\n", apiResp.GoodsNum) + } + if apiResp.PNum != "" { + fmt.Printf("📄 页码: %s\n", apiResp.PNum) + } + } else { + fmt.Printf("❌ 失败: %s\n", apiResp.Error) + } + } else { + fmt.Printf("原始响应: %s\n", result) + // 如果是代理地址,可以进一步处理 + if len(result) > 0 && (result[0:4] == "http" || result[0:3] == "socks") { + fmt.Printf("✅ 代理服务器地址获取成功\n") + fmt.Printf("🔗 代理地址: %s\n", result) + } + } + return count +} + +// 专门处理代理配置响应的函数 +func handleProxyResponse(result string, err error) { + fmt.Printf("\n代理配置测试:\n") + fmt.Println("=" + string(make([]byte, 50)) + "=") + + if err != nil { + fmt.Printf("❌ 错误: %v\n", err) + return + } + + // 代理配置通常返回代理服务器地址,不是JSON格式 + fmt.Printf("📡 代理服务器地址: %s\n", result) + fmt.Printf("✅ 代理配置获取成功\n") + + // 如果需要,可以在这里进一步处理代理地址 + // 例如:验证代理格式、提取IP和端口等 + if result != "" { + fmt.Printf("🔗 可用代理: %s\n", result) + } +} + +// // ProxyTypeManager 调用代理类型管理器 +// +// func (m *DLLManager) ProxyTypeManager(proxyType, username, password, machineCode string) (string, error) { +// proc, err := m.dll.FindProc("ProxyTypeManager") +// if err != nil { +// return "", fmt.Errorf("找不到函数 ProxyTypeManager: %v", err) +// } +// +// // 准备参数 +// proxyTypePtr, _ := syscall.BytePtrFromString(proxyType) +// usernamePtr, _ := syscall.BytePtrFromString(username) +// passwordPtr, _ := syscall.BytePtrFromString(password) +// machineCodePtr, _ := syscall.BytePtrFromString(machineCode) +// +// // 调用函数 +// r1, _, err := proc.Call( +// uintptr(unsafe.Pointer(proxyTypePtr)), +// uintptr(unsafe.Pointer(usernamePtr)), +// uintptr(unsafe.Pointer(passwordPtr)), +// uintptr(unsafe.Pointer(machineCodePtr)), +// ) +// +// if err != nil && err.Error() != "The operation completed successfully." { +// return "", fmt.Errorf("调用 ProxyTypeManager 失败: %v", err) +// } +// +// // 转换结果 +// result := (*byte)(unsafe.Pointer(r1)) +// var resultBytes []byte +// for i := 0; ; i++ { +// bytePtr := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(result)) + uintptr(i))) +// if *bytePtr == 0 { +// break +// } +// resultBytes = append(resultBytes, *bytePtr) +// } +// +// // 释放内存 +// freeProc, _ := m.dll.FindProc("FreeString") +// if freeProc != nil { +// freeProc.Call(r1) +// } +// +// return string(resultBytes), nil +// } +// +// FreeCString 释放C字符串内存 +func (m *DLLManager) FreeCString(strPtr uintptr) error { + proc, err := m.dll.FindProc("FreeCString") + if err != nil { + return fmt.Errorf("找不到函数 FreeCString: %v", err) + } + + _, _, err = proc.Call(strPtr) + if err != nil && err.Error() != "The operation completed successfully." { + return fmt.Errorf("调用 FreeCString 失败: %v", err) + } + + return nil +} + +func main() { + http.HandleFunc("/api/kfzShopBookInfo", handleGetKFZShopBookInfo) + port := "8080" + server := &http.Server{ + Addr: ":" + port, + Handler: nil, + } + // 启动服务器 + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("服务器启动失败: %v\n", err) + } +} + +func handleGetKFZShopBookInfo(w http.ResponseWriter, r *http.Request) { + // 加载DLL + manager, err := NewDLLManager("dll/xkongfz.dll") + if err != nil { + fmt.Printf("初始化DLL管理器失败: %v", err) + } + //defer manager.Close() + + log.Println("✅ DLL加载成功") + + // 使用默认配置初始化 + config := createDefaultConfig() + initializeConfig(config) + configJSON, err := json.Marshal(config) + if err != nil { + fmt.Printf("序列化配置失败: %v", err) + } + + result, err := manager.Initialize(string(configJSON)) + if err != nil { + fmt.Printf("初始化失败: %v", err) + } + + var initResp APIResponses + if err := json.Unmarshal([]byte(result), &initResp); err != nil { + fmt.Printf("解析初始化响应失败: %v", err) + } + + if !initResp.Success { + fmt.Printf("初始化失败: %s", initResp.Message) + } + + log.Println("✅ DLL初始化成功") + // 设置响应头 + w.Header().Set("Content-Type", "application/json; charset=utf-8") + + // 只支持GET请求 + if r.Method != http.MethodGet { + sendErrorResponse(w, http.StatusMethodNotAllowed, "只支持GET方法") + return + } + + login, err := outLogin("15140030829", "cuiyang") + if err != nil { + fmt.Printf(err.Error()) + } + fmt.Println(login) + + msg, err := outGetGoodsTplMsg(login, "1181761", "") + if err != nil { + fmt.Printf(err.Error()) + } + fmt.Println(msg) + + //// 获取代理信息 + //typeManager, err := proxyTypeManager("CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f", "") + //if err != nil { + // fmt.Printf(err.Error()) + //} + //fmt.Println(typeManager) + + //// 获取cookie + //cookie, err := loginCookie() + //if err != nil { + // fmt.Printf(err.Error()) + //} + //fmt.Println(cookie) + + //account, err := getRandomAccount() + //if err != nil { + // fmt.Printf(err.Error()) + //} + //fmt.Println(account) + + //var isbns = []string{ + // "9787500601593", "9787506331746", "9787020106752", "9787807089353", "9787536692930", "9787530221532", "9787544270878", "9787208061644", "9787506365437", "9787513708371", "9787513922135", "9787536693968", "9787541151736", "9787544213561", "9787544754781", "9787549204304", "9787108006639", "9787531764700", "9787544253994", "9787540456023", "9787540456429", "9787544267618", "9787544277723", "9787550008496", "9787806070680", "9787806801529", "9787807530244", "9787020008728", "9787020139590", "9787204055401", "9787500680239", "9787505724778", "9787506365680", "9787530221525", "9787536097261", + //} + // + //for _, isbn := range isbns { + // // 获取孔网图片 + // isbnss, err := outGetImageByIsbn("", isbn, "", 0, 0) + // if err != nil { + // fmt.Printf(err.Error()) + // } + // fmt.Println(isbnss) + //} + + //books, num, pNum, err := outGetGoodsListMsgByShopId(3092, + // typeManager, true, "", + // "", 0, 14, 1, 5) + + //msg, err := manager.OutGetTopGoodsListMsg(0, "") + //if err != nil { + // + //} + //fmt.Println(msg) + //fetchMode := r.URL.Query().Get("fetchMode") + //proxyType := r.URL.Query().Get("proxyType") + //username := r.URL.Query().Get("username") + //password := r.URL.Query().Get("password") + //machineCode := r.URL.Query().Get("machineCode") + //url := r.URL.Query().Get("url") + //shopId := r.URL.Query().Get("shopId") //https://shop.kongfz.com/793900/all/0_100_0_0_1_sort_desc_0_0/ + //isImage := r.URL.Query().Get("isImage") + //bookNum := r.URL.Query().Get("bookNum") + //pageNum := r.URL.Query().Get("pageNum") + //sortType := r.URL.Query().Get("sortType") + //sort := r.URL.Query().Get("sort") + //priceDown := r.URL.Query().Get("priceDown") + //priceUp := r.URL.Query().Get("priceUp") + + //details, err := manager.OutGetGoodsMsgByDetailUrl("https://book.kongfz.com/151391/6531924071/", "") + //if err != nil { + // + //} + //fmt.Println(details) + + //id, err := manager.OutGetGoodsListMsgByShopId(1181761, + // "", true, "", + // "", 0, 0, 1, 5) + //if err != nil { + // fmt.Println(err) + //} + //fmt.Println(id) + //var pa ShopBookResult + //if err := json.Unmarshal([]byte(id), &pa); err != nil { + // log.Printf("[ERROR] 解析第 %d 页失败: %v", pa, err) + //} + //for _, urls := range pa.Data.Data { + // ss, err := proxyTypeManager("CALF_ELEPHANT_PROXY", "1297757178467602432", "QgQBvP7f", "") + // if err != nil { + // fmt.Printf(err.Error()) + // } + // fmt.Println(ss) + // url, err := manager.OutGetGoodsMsgByDetailUrl(urls.DetailURL, ss) + // if err != nil { + // fmt.Println(err) + // } + // fmt.Println(url) + //} + + //books, num, pNum, err := outGetGoodsListMsgByShopId(1181761, + // typeManager, true, "", + // "", 0, 0, 1, 5) + // + //fmt.Println(books) + //fmt.Println(num) + //fmt.Println(pNum) + //fmt.Println(err) + //for _, book := range books { + // ss, err := proxyTypeManager("TAIL_PROXY", "", "", "07f4d0fbcff99966c2b37b0c1fb7f01c") + // if err != nil { + // fmt.Printf(err.Error()) + // } + // fmt.Println(ss) + // url, err := outGetGoodsMsgByDetailUrl(book.DetailUrl, ss) + // if err != nil { + // fmt.Println(err) + // } + // fmt.Println(url) + //} + +} + +// 发送错误响应 +func sendErrorResponse(w http.ResponseWriter, statusCode int, message string) { + response := APIResp{ + Success: false, + Message: message, + } + w.WriteHeader(statusCode) + json.NewEncoder(w).Encode(response) +} + +type ShopBookResult struct { + Success bool `json:"success"` + Message string `json:"message"` + Data struct { + GoodsNum string `json:"goods_num"` + Pnum string `json:"pnum"` + Data []struct { + BookName string `json:"book_name"` + Author string `json:"author"` + Publisher string `json:"publisher"` + ISBN string `json:"isbn"` + PublishTime int64 `json:"publication_time"` + Edition string `json:"edition"` + PrintTime string `json:"print_time"` + FixPrice string `json:"fix_price"` + BindingLayout string `json:"binding_layout"` + Format string `json:"format"` + Paper string `json:"paper"` + Pages string `json:"pages"` + Wordage string `json:"wordage"` + Languages string `json:"languages"` + Era string `json:"era"` + EngravingMethod string `json:"engraving_method"` + Dimensions string `json:"dimensions"` + VolumeNumber string `json:"volume_number"` + BookPic string `json:"book_pic"` + BookPicS string `json:"book_pic_s"` + SellingPrice string `json:"selling_price"` + Condition string `json:"condition"` + ExpressDeliveryFee string `json:"express_delivery_fee"` + Editor string `json:"editor"` + Category string `json:"category"` + BuyCount string `json:"buy_count"` + SellCount string `json:"sell_count"` + Content string `json:"content"` + Mid int `json:"mid"` + ItemID int64 `json:"item_id"` + ShopID int `json:"shop_id"` + DetailURL string `json:"detail_url"` + } `json:"data"` + } `json:"data"` +}