334 lines
9.9 KiB
Go
334 lines
9.9 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"kfz-goods-pricing/internal/config"
|
||
"log"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/parnurzeal/gorequest"
|
||
"kfz-goods-pricing/internal/model"
|
||
"kfz-goods-pricing/internal/repository"
|
||
)
|
||
|
||
// GoodsService 商品服务
|
||
type GoodsService struct {
|
||
proxy string
|
||
apiRateLimitSeconds int
|
||
rateLimitMu sync.Mutex
|
||
lastAPICall time.Time
|
||
goodsRepository *repository.GoodsRepository
|
||
callbackURL string
|
||
tokenRepository *repository.TokenRepository
|
||
currentTokenIndex int
|
||
tokenMu sync.Mutex
|
||
}
|
||
|
||
// NewGoodsService 创建商品服务实例
|
||
func NewGoodsService(proxy string, apiRateLimitSeconds int, callbackURL string, tokenRepo *repository.TokenRepository) *GoodsService {
|
||
return &GoodsService{
|
||
proxy: proxy,
|
||
apiRateLimitSeconds: apiRateLimitSeconds,
|
||
goodsRepository: repository.NewGoodsRepository(),
|
||
callbackURL: callbackURL,
|
||
tokenRepository: tokenRepo,
|
||
currentTokenIndex: 0,
|
||
}
|
||
}
|
||
|
||
// QueryRequest 查询请求参数 {"isbn":"9787802204461","out_id":"132456","quality":"100"}
|
||
type QueryRequest struct {
|
||
ISBN string `json:"isbn"` // isbn
|
||
OutID string `json:"out_id"` // 输出ID
|
||
Quality string `json:"quality"` // 品相
|
||
QueryIndex int `json:"query_index"` // 排第几位
|
||
UserID string `json:"user_id"` // 用户ID
|
||
PlaceholderDownPrice float64 `json:"placeholder_down_price"` // 占位降价
|
||
MinShippingFee float64 `json:"min_shipping_fee"` // 最低运费
|
||
MinPrice float64 `json:"min_price"` // 最低书价
|
||
}
|
||
|
||
// QueryResponse 查询响应
|
||
type QueryResponse struct {
|
||
Code int `json:"code"`
|
||
Message string `json:"message"`
|
||
ID int64 `json:"id,omitempty"`
|
||
Data interface{} `json:"data,omitempty"`
|
||
}
|
||
|
||
// QueryGoods 查询商品信息
|
||
func (s *GoodsService) QueryGoods(req *QueryRequest) *QueryResponse {
|
||
if req.ISBN == "0" {
|
||
return &QueryResponse{
|
||
Code: 200,
|
||
Message: "success",
|
||
}
|
||
}
|
||
|
||
// 插入入参记录到数据库
|
||
id, err := s.goodsRepository.Insert(req.ISBN, req.OutID, req.Quality, req.UserID, req.QueryIndex, req.PlaceholderDownPrice, req.MinShippingFee, req.MinPrice)
|
||
if err != nil {
|
||
return &QueryResponse{
|
||
Code: 500,
|
||
Message: fmt.Sprintf("保存记录失败: %v", err),
|
||
}
|
||
}
|
||
|
||
return &QueryResponse{
|
||
Code: 200,
|
||
Message: "success",
|
||
ID: id,
|
||
}
|
||
}
|
||
|
||
// rateLimitWait 限流等待
|
||
func (s *GoodsService) rateLimitWait() {
|
||
if s.apiRateLimitSeconds <= 0 {
|
||
return
|
||
}
|
||
|
||
s.rateLimitMu.Lock()
|
||
defer s.rateLimitMu.Unlock()
|
||
|
||
elapsed := time.Since(s.lastAPICall)
|
||
if elapsed < time.Duration(s.apiRateLimitSeconds)*time.Second {
|
||
time.Sleep(time.Duration(s.apiRateLimitSeconds)*time.Second - elapsed)
|
||
}
|
||
s.lastAPICall = time.Now()
|
||
}
|
||
|
||
// sendCallback 发送回调请求
|
||
func (s *GoodsService) sendCallback(outID, userID string, price, shippingFee float64) {
|
||
if s.callbackURL == "" {
|
||
return
|
||
}
|
||
|
||
// price*100 转int, shipping_fee*100 转int
|
||
salePrice := int(price * 100)
|
||
cost := int(shippingFee * 100)
|
||
|
||
request := gorequest.New()
|
||
if s.proxy != "" {
|
||
request.Proxy(s.proxy)
|
||
}
|
||
_, _, errs := request.Post(s.callbackURL).
|
||
Set("Content-Type", "application/x-www-form-urlencoded").
|
||
Send(fmt.Sprintf("product_id=%s&user_id=%s&sale_price=%d&cost=%d", outID, userID, salePrice, cost)).
|
||
Timeout(30 * time.Second).
|
||
End()
|
||
|
||
if len(errs) > 0 {
|
||
log.Printf("回调失败: %v", errs)
|
||
} else {
|
||
log.Printf("回调成功: product_id=%s, user_id=%s, sale_price=%d, cost=%d", outID, userID, salePrice, cost)
|
||
}
|
||
}
|
||
|
||
// StartTimerScheduler 启动定时器
|
||
func (s *GoodsService) StartTimerScheduler(intervalSeconds int) {
|
||
ticker := time.NewTicker(time.Duration(intervalSeconds) * time.Second)
|
||
go func() {
|
||
for range ticker.C {
|
||
s.syncGoodsPricing()
|
||
}
|
||
}()
|
||
}
|
||
|
||
// syncGoodsPricing 定时同步商品价格
|
||
func (s *GoodsService) syncGoodsPricing() {
|
||
cfg := config.GetGlobal()
|
||
|
||
// 查询一条记录,按fail_count升序、updated_at倒序
|
||
record, err := s.goodsRepository.GetAllOrderByUpdatedAt()
|
||
if err != nil {
|
||
log.Printf("定时任务查询失败: %v", err)
|
||
return
|
||
}
|
||
if record == nil {
|
||
return
|
||
}
|
||
|
||
// 限流等待
|
||
s.rateLimitWait()
|
||
|
||
var price float64
|
||
var shippingFee float64
|
||
// 最终书价
|
||
var finalPrice float64
|
||
|
||
// 查询孔网数据
|
||
bookInfo, err := s.outGetAllGoods(record.ISBN, record.Quality, record.QueryIndex)
|
||
if err != nil {
|
||
if cfg.NewPrice == 0 {
|
||
s.goodsRepository.MarkFailed(record.ID)
|
||
log.Printf("定时任务[%d]查询孔网失败(fail_count=%d): %v", record.ID, record.FailCount+1, err)
|
||
return
|
||
}
|
||
price = cfg.NewPrice
|
||
} else {
|
||
// 查询成功,更新价格
|
||
price, _ = strconv.ParseFloat(bookInfo.Price, 64)
|
||
shippingFee, _ = strconv.ParseFloat(bookInfo.ShippingFee, 64)
|
||
totalPrice := price + shippingFee
|
||
finalPrice = totalPrice - record.PlaceholderDownPrice - record.MinShippingFee
|
||
if finalPrice < record.MinPrice {
|
||
finalPrice = record.MinPrice
|
||
}
|
||
// 保留两位小数
|
||
finalPrice, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", finalPrice), 64)
|
||
}
|
||
|
||
if err := s.goodsRepository.UpdatePrice(record.ID, price, shippingFee, finalPrice); err != nil {
|
||
log.Printf("定时任务[%d]更新价格失败: %v", record.ID, err)
|
||
return
|
||
}
|
||
|
||
log.Printf("定时任务[%d]更新成功: price=%.2f, shipping_fee=%.2f", record.ID, price, shippingFee)
|
||
|
||
// 调用回调
|
||
s.sendCallback(record.OutID, record.UserID, finalPrice, record.MinShippingFee)
|
||
}
|
||
|
||
// outGetAllGoods 爬取孔网所有商品页面
|
||
func (s *GoodsService) outGetAllGoods(isbn string, quality string, queryIndex int) (*model.BookInfo, error) {
|
||
var actionPathList []string
|
||
|
||
// 从数据库获取启用的token列表
|
||
tokens, err := s.tokenRepository.GetEnabledTokens()
|
||
if err != nil || len(tokens) == 0 {
|
||
return nil, fmt.Errorf("没有可用的token")
|
||
}
|
||
|
||
// 轮询选择token
|
||
s.tokenMu.Lock()
|
||
s.currentTokenIndex = s.currentTokenIndex % len(tokens)
|
||
currentIdx := s.currentTokenIndex
|
||
s.currentTokenIndex++
|
||
s.tokenMu.Unlock()
|
||
|
||
token := tokens[currentIdx].Token
|
||
|
||
kfzUrl := "https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list?dataType=0&page=1&sortType=7&quaSelect=2"
|
||
//kfzUrl := "https://search.kongfz.com/pc-gw/search-web/client/pc/product/keyword/list?dataType=0&page=1&sortType=7&userArea=13003000000&quaSelect=2"
|
||
|
||
// 整理查询参数-加上排序
|
||
actionPathList = append(actionPathList, "sortType")
|
||
|
||
// isbn
|
||
if isbn != "" {
|
||
kfzUrl = kfzUrl + "&keyword=" + isbn
|
||
}
|
||
|
||
// 品相
|
||
if quality != "" {
|
||
kfzUrl = kfzUrl + "&quality=" + quality + "~"
|
||
actionPathList = append(actionPathList, "quality")
|
||
}
|
||
|
||
// 参数进行分割
|
||
actionPath := strings.Join(actionPathList, ",")
|
||
// 加入查询参数
|
||
kfzUrl = kfzUrl + "&actionPath=" + actionPath
|
||
|
||
// 创建HTTP平台端
|
||
requestSpt := gorequest.New()
|
||
|
||
// 设置代理(如果有提供代理URL)
|
||
if s.proxy != "" {
|
||
requestSpt.Proxy(s.proxy)
|
||
}
|
||
fmt.Println("请求地址:", kfzUrl)
|
||
// 发送请求
|
||
respSpt, bodySpt, errsSpt := requestSpt.Get(kfzUrl).
|
||
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://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)
|
||
}
|
||
|
||
fmt.Println("响应数据:", bodySpt)
|
||
|
||
// 解析响应
|
||
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 {
|
||
Title string `json:"title"`
|
||
Author string `json:"author"`
|
||
Press string `json:"press"`
|
||
PubDateText string `json:"pubDateText"`
|
||
Isbn string `json:"isbn"`
|
||
Price float64 `json:"price"`
|
||
Postage struct {
|
||
ShippingList []struct {
|
||
ShippingFee float64 `json:"shippingFee"`
|
||
} `json:"shippingList"`
|
||
} `json:"postage"`
|
||
} `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.Status != 1 {
|
||
return nil, fmt.Errorf("错误信息: %v,状态码: %s", apiSptResp.Message, apiSptResp.ErrType)
|
||
}
|
||
|
||
bookInfo := &model.BookInfo{}
|
||
|
||
if apiSptResp.Data.ItemResponse.Total > 0 && len(apiSptResp.Data.ItemResponse.List) > 0 {
|
||
if queryIndex > 0 && queryIndex <= len(apiSptResp.Data.ItemResponse.List) {
|
||
goodsInfo := apiSptResp.Data.ItemResponse.List[queryIndex-1]
|
||
bookInfo.BookName = goodsInfo.Title
|
||
bookInfo.ISBN = goodsInfo.Isbn
|
||
bookInfo.Author = goodsInfo.Author
|
||
bookInfo.Publisher = goodsInfo.Press
|
||
bookInfo.PubDate = goodsInfo.PubDateText
|
||
bookInfo.Price = fmt.Sprintf("%.2f", goodsInfo.Price)
|
||
for _, shipping := range goodsInfo.Postage.ShippingList {
|
||
bookInfo.ShippingFee = fmt.Sprintf("%.2f", shipping.ShippingFee)
|
||
}
|
||
} else {
|
||
goodsInfo := apiSptResp.Data.ItemResponse.List[len(apiSptResp.Data.ItemResponse.List)-1]
|
||
bookInfo.BookName = goodsInfo.Title
|
||
bookInfo.ISBN = goodsInfo.Isbn
|
||
bookInfo.Author = goodsInfo.Author
|
||
bookInfo.Publisher = goodsInfo.Press
|
||
bookInfo.PubDate = goodsInfo.PubDateText
|
||
bookInfo.Price = fmt.Sprintf("%.2f", goodsInfo.Price)
|
||
for _, shipping := range goodsInfo.Postage.ShippingList {
|
||
bookInfo.ShippingFee = fmt.Sprintf("%.2f", shipping.ShippingFee)
|
||
}
|
||
}
|
||
return bookInfo, nil
|
||
}
|
||
|
||
return nil, fmt.Errorf("查询失败,没有数据!")
|
||
}
|