2998 lines
93 KiB
Go
2998 lines
93 KiB
Go
package main
|
||
|
||
/*
|
||
#include <stdlib.h>
|
||
|
||
// 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() {
|
||
//
|
||
//}
|