generated from kedaya_haitao/template
Some checks failed
Build image / build (push) Failing after 49s
1. 修改ExportCheapProduct方法,支持直接将Excel数据写入HTTP响应 2. 添加图片下载和嵌入功能,提升Excel报表可视化效果 3. 优化数据查询和处理逻辑,提高导出效率 4. 完善错误处理和日志记录 此次修改避免了临时文件的创建和管理,减少了内存使用,提高了响应速度。
295 lines
8.9 KiB
Go
295 lines
8.9 KiB
Go
package coach_outlet
|
||
|
||
import (
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gitea.timerzz.com/kedaya_haitao/common/structs/storage"
|
||
"gitea.timerzz.com/kedaya_haitao/common/structs/utils"
|
||
v2 "gitea.timerzz.com/kedaya_haitao/common/structs/v2"
|
||
dw_sdk "gitea.timerzz.com/kedaya_haitao/dw-sdk"
|
||
"github.com/gofiber/fiber/v3"
|
||
"github.com/samber/lo"
|
||
"github.com/sirupsen/logrus"
|
||
"github.com/xuri/excelize/v2"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
const (
|
||
USCoachOutlet = "us-coach-outlet"
|
||
CACoachOutlet = "ca-coach-outlet"
|
||
)
|
||
|
||
type Exporter struct {
|
||
storage *storage.Storage
|
||
dw *dw_sdk.Client
|
||
seller v2.Seller
|
||
}
|
||
|
||
func NewExporter(storage *storage.Storage, dw *dw_sdk.Client) (e *Exporter, err error) {
|
||
e = &Exporter{storage: storage, dw: dw}
|
||
e.seller, err = storage.Seller().GetBySellerId("dw-normal")
|
||
return e, err
|
||
}
|
||
|
||
// ExportCheapProduct 导出价格较低的商品数据到Excel,直接写入到HTTP响应
|
||
func (e *Exporter) ExportCheapProduct(providerId string, c fiber.Ctx) error {
|
||
// 创建Excel文件
|
||
f := excelize.NewFile()
|
||
defer f.Close()
|
||
|
||
// 创建工作表
|
||
sheetName := providerId
|
||
_, err := f.NewSheet(sheetName)
|
||
if err != nil {
|
||
return fmt.Errorf("创建工作表失败: %w", err)
|
||
}
|
||
_ = f.DeleteSheet("Sheet1") // 删除默认的Sheet1
|
||
|
||
// 确定查询的供应商ID
|
||
var cheapId, otherId string
|
||
cheapId = providerId
|
||
otherId = lo.Ternary(providerId == CACoachOutlet, USCoachOutlet, CACoachOutlet)
|
||
|
||
// 查询符合条件的商品
|
||
var articles []*v2.Article
|
||
rowIndex := 1 // 从第1行开始(标题行)
|
||
setHeader(f, sheetName)
|
||
err = e.storage.DB().Model(&v2.Article{}).Select("DISTINCT articles.*").
|
||
Preload("Providers", func(db *gorm.DB) *gorm.DB {
|
||
return db.Where("provider_id IN ?", []string{cheapId, otherId})
|
||
}).
|
||
Preload("Sellers", "seller_id = ?", "dw-normal").
|
||
Joins("JOIN provider_articles pa1 ON articles.id = pa1.article_id AND pa1.provider_id = ? AND pa1.available = true", cheapId).
|
||
Joins("JOIN provider_articles pa2 ON articles.id = pa2.article_id AND pa2.provider_id = ?", otherId).
|
||
Joins("JOIN seller_articles sa ON articles.id = sa.article_id AND (sa.seller_id = 'dw-normal' AND (sa.exclude = false OR sa.exclude IS NULL))").
|
||
Where("pa1.cost->>'finalPrice' <= pa2.cost->>'finalPrice'").
|
||
FindInBatches(&articles, 20, func(tx *gorm.DB, batch int) error {
|
||
for _, article := range articles {
|
||
// 更新DW价格(如果需要)
|
||
dwSeller, idx, exist := lo.FindIndexOf(article.Sellers, func(seller v2.SellerArticle) bool {
|
||
return seller.SellerId == "dw-normal"
|
||
})
|
||
if exist && dwSeller.Sell.CreatedAt.Before(time.Now().AddDate(0, 0, -1)) {
|
||
// 如果已经是一天前的数据,那么就要更新下
|
||
dwSeller = e.pullDwPrice(dwSeller)
|
||
article.Sellers[idx] = dwSeller
|
||
}
|
||
|
||
// 计算利润率
|
||
utils.ProfitRate(article)
|
||
|
||
// 添加到Excel
|
||
rowIndex++
|
||
if err = addRow(f, sheetName, rowIndex, article); err != nil {
|
||
logrus.Errorf("添加行失败: %v", err)
|
||
continue
|
||
}
|
||
}
|
||
return nil
|
||
}).Error
|
||
|
||
if err != nil {
|
||
return fmt.Errorf("查询数据失败: %w", err)
|
||
}
|
||
|
||
// 保存Excel文件
|
||
filename := fmt.Sprintf("coach_outlet_%s.xlsx", providerId)
|
||
|
||
if err = f.SaveAs(filename); err != nil {
|
||
return fmt.Errorf("保存Excel文件失败: %w", err)
|
||
}
|
||
|
||
// 直接将Excel文件写入到HTTP响应
|
||
if err = f.Write(c.Response().BodyWriter()); err != nil {
|
||
return fmt.Errorf("写入Excel数据失败: %w", err)
|
||
}
|
||
|
||
logrus.Infof("成功导出 %d 条数据", rowIndex-1)
|
||
return nil
|
||
}
|
||
|
||
// 设置标题行
|
||
func setHeader(f *excelize.File, sheetName string) {
|
||
// 设置列标题(如果是第一行)
|
||
headers := []string{"商品名", "图片", "美国官网原价", "美国官网最终价格",
|
||
"加拿大官网原价", "加拿大官网最终价格", "DW价格", "DW到手价格", "利润", "利润率"}
|
||
for i, header := range headers {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
_ = f.SetCellValue(sheetName, cell, header)
|
||
}
|
||
|
||
// 设置列宽
|
||
_ = f.SetColWidth(sheetName, "A", "A", 30)
|
||
_ = f.SetColWidth(sheetName, "B", "B", 20) // 增加图片列宽度
|
||
_ = f.SetColWidth(sheetName, "C", "J", 15)
|
||
_ = f.SetRowHeight(sheetName, 1, 20) // 设置标题行高
|
||
|
||
// 设置样式
|
||
style, _ := f.NewStyle(&excelize.Style{
|
||
Font: &excelize.Font{Bold: true},
|
||
Alignment: &excelize.Alignment{Horizontal: "center"},
|
||
})
|
||
_ = f.SetRowStyle(sheetName, 1, 1, style)
|
||
}
|
||
|
||
// addRow 根据article信息添加一行到excel
|
||
// 商品名,图片,美国官网原价,美国官网最终价格,加拿大官网原始价格,加拿大官网最终价格,dw价格, dw到手价格,利润,利润率
|
||
func addRow(f *excelize.File, sheetName string, rowIndex int, article *v2.Article) error {
|
||
// 获取美国和加拿大的供应商信息
|
||
usProvider, _ := lo.Find(article.Providers, func(p v2.ProviderArticle) bool {
|
||
return p.ProviderId == USCoachOutlet
|
||
})
|
||
caProvider, _ := lo.Find(article.Providers, func(p v2.ProviderArticle) bool {
|
||
return p.ProviderId == CACoachOutlet
|
||
})
|
||
|
||
// 获取DW销售商信息
|
||
dwSeller, _, _ := lo.FindIndexOf(article.Sellers, func(s v2.SellerArticle) bool {
|
||
return s.SellerId == "dw-normal"
|
||
})
|
||
|
||
// 填充数据
|
||
cells := []interface{}{
|
||
article.Name,
|
||
"", // 图片单元格留空,后面会添加图片
|
||
usProvider.Cost.OriginalPrice,
|
||
usProvider.Cost.FinalPrice,
|
||
caProvider.Cost.OriginalPrice,
|
||
caProvider.Cost.FinalPrice,
|
||
dwSeller.Sell.OriginalPrice,
|
||
dwSeller.Sell.FinalPrice,
|
||
article.GrossProfit,
|
||
article.Rate,
|
||
}
|
||
|
||
// 写入数据
|
||
for i, value := range cells {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, rowIndex)
|
||
_ = f.SetCellValue(sheetName, cell, value)
|
||
}
|
||
|
||
// 设置行高以适应图片
|
||
_ = f.SetRowHeight(sheetName, rowIndex, 80)
|
||
|
||
// 处理图片
|
||
if len(article.Image) > 0 {
|
||
imgCell, _ := excelize.CoordinatesToCellName(2, rowIndex)
|
||
|
||
// 下载图片
|
||
imgData, err := downloadImage(article.Image)
|
||
if err != nil {
|
||
logrus.Warnf("下载图片失败 %s: %v", article.Image, err)
|
||
// 如果下载失败,至少显示URL
|
||
_ = f.SetCellValue(sheetName, imgCell, article.Image)
|
||
} else {
|
||
// 添加图片到Excel
|
||
if err := f.AddPictureFromBytes(sheetName, imgCell, &excelize.Picture{
|
||
Extension: getImageExtension(article.Image),
|
||
File: imgData,
|
||
Format: &excelize.GraphicOptions{
|
||
AutoFit: true,
|
||
ScaleX: 0.5,
|
||
ScaleY: 0.5,
|
||
Positioning: "oneCell",
|
||
},
|
||
}); err != nil {
|
||
logrus.Warnf("添加图片到Excel失败: %v", err)
|
||
// 如果添加失败,显示URL
|
||
_ = f.SetCellValue(sheetName, imgCell, article.Image)
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// downloadImage 下载图片并返回字节数据
|
||
func downloadImage(url string) ([]byte, error) {
|
||
// 创建HTTP客户端,设置超时
|
||
client := &http.Client{
|
||
Timeout: 10 * time.Second,
|
||
}
|
||
|
||
// 发送GET请求
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("HTTP请求失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查状态码
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, fmt.Errorf("HTTP状态码错误: %d", resp.StatusCode)
|
||
}
|
||
|
||
// 读取响应体
|
||
imgData, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("读取图片数据失败: %w", err)
|
||
}
|
||
|
||
return imgData, nil
|
||
}
|
||
|
||
// getImageExtension 从URL获取图片扩展名
|
||
func getImageExtension(url string) string {
|
||
// 默认扩展名
|
||
ext := ".jpg"
|
||
|
||
// 从URL中提取扩展名
|
||
if strings.Contains(url, ".") {
|
||
parts := strings.Split(url, ".")
|
||
lastPart := parts[len(parts)-1]
|
||
|
||
// 处理可能的查询参数
|
||
if strings.Contains(lastPart, "?") {
|
||
lastPart = strings.Split(lastPart, "?")[0]
|
||
}
|
||
|
||
// 常见图片扩展名
|
||
switch strings.ToLower(lastPart) {
|
||
case "jpg", "jpeg", "png", "gif", "bmp", "webp":
|
||
ext = "." + strings.ToLower(lastPart)
|
||
}
|
||
}
|
||
|
||
return ext
|
||
}
|
||
|
||
func (e *Exporter) pullDwPrice(sArticle v2.SellerArticle) v2.SellerArticle {
|
||
skuId, _ := strconv.Atoi(sArticle.SkuID)
|
||
resp, err := e.dw.NormalBidClient().LowestPrice(skuId)
|
||
if err != nil {
|
||
return sArticle
|
||
}
|
||
lowest, exist := lo.Find(resp.Items, func(item dw_sdk.LowestPriceItem) bool {
|
||
return item.BiddingType == dw_sdk.BiddingType_Normal
|
||
})
|
||
if !exist {
|
||
// 没有拿到对应类型的价格,那就拿一个最低的
|
||
lowest = lo.MinBy(resp.Items, func(item1, item2 dw_sdk.LowestPriceItem) bool {
|
||
return item1.LowestPrice < item2.LowestPrice && item1.LowestPrice > 0
|
||
})
|
||
}
|
||
if lowest.LowestPrice == 0 {
|
||
// 没有拿到价格,将exclude设置为true,如果后面要拉取,需要手动打开
|
||
return sArticle
|
||
}
|
||
sell := utils.CalculateSellerPrice(append(e.seller.CalculateProcess, sArticle.CalculateProcess...), map[string]float64{
|
||
"originalPrice": float64(lowest.LowestPrice / 100),
|
||
})
|
||
if sell.OriginalPrice != sArticle.Sell.OriginalPrice {
|
||
sArticle.Sell = sell
|
||
sArticle.HistoryPrice = append(sArticle.HistoryPrice, sArticle.Sell)
|
||
}
|
||
if err = e.storage.SellerArticle().Upsert(sArticle); err != nil {
|
||
logrus.Errorf("保存sellerArticle失败:%v", err)
|
||
}
|
||
return sArticle
|
||
}
|