feat: 优化Coach Outlet Excel导出功能
Some checks failed
Build image / build (push) Failing after 49s

1. 修改ExportCheapProduct方法,支持直接将Excel数据写入HTTP响应
2. 添加图片下载和嵌入功能,提升Excel报表可视化效果
3. 优化数据查询和处理逻辑,提高导出效率
4. 完善错误处理和日志记录

此次修改避免了临时文件的创建和管理,减少了内存使用,提高了响应速度。
This commit is contained in:
timerzz 2025-03-30 17:36:10 +08:00
parent b0b6522902
commit be5c82683f
9 changed files with 715 additions and 276 deletions

View File

@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/bash
# 打包 Helm Chart
helm package chart/

View File

@ -15,10 +15,10 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 1.0.0
version: 1.0.2
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v1.0.0"
appVersion: "v1.0.2"

View File

@ -68,6 +68,8 @@ ingress:
paths:
- path: /api/v2/articles
pathType: ImplementationSpecific
- path: /api/v2/tools/excel
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
@ -125,6 +127,8 @@ volumes:
items:
- key: db
path: db.yaml
- key: dw
path: dw.yaml
defaultMode: 420
# - name: foo
# secret:

View File

@ -1,56 +1,64 @@
package main
import (
"context"
"flag"
"os"
"os/signal"
"gitea.timerzz.com/kedaya_haitao/article/service"
"gitea.timerzz.com/kedaya_haitao/common/pkg/database"
"gitea.timerzz.com/kedaya_haitao/common/pkg/web"
"gitea.timerzz.com/kedaya_haitao/common/structs/storage"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/cors"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/golang/glog"
)
func main() {
flag.Parse()
glog.Info(">>> BEGIN INIT<<<")
ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt)
defer cancel()
// 初始化数据库
db, err := database.InitDefaultDatabase()
if err != nil {
glog.Fatalf("init database failed: %v", err)
}
// 初始化服务
r := fiber.New(fiber.Config{ErrorHandler: web.ErrHandle})
r.Use(cors.New(), recover.New())
stg := storage.NewStorage(db)
_ = stg.Article().AutoMigrate()
svc := []web.Register{
web.NewProbe(),
service.NewArticle(stg),
}
for _, s := range svc {
s.Registry(r)
}
port := os.Getenv("PORT")
if port == "" {
port = "80"
}
if err = r.Listen(":"+port, fiber.ListenConfig{
EnablePrintRoutes: true,
GracefulContext: ctx,
}); err != nil {
glog.Warningf("service over: %v", err)
}
}
package main
import (
"context"
"flag"
"os"
"os/signal"
"gitea.timerzz.com/kedaya_haitao/article/service"
"gitea.timerzz.com/kedaya_haitao/common/pkg/database"
"gitea.timerzz.com/kedaya_haitao/common/pkg/web"
"gitea.timerzz.com/kedaya_haitao/common/structs/storage"
dw_sdk "gitea.timerzz.com/kedaya_haitao/dw-sdk"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/cors"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/sirupsen/logrus"
"github.com/golang/glog"
)
func main() {
flag.Parse()
glog.Info(">>> BEGIN INIT<<<")
ctx, cancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt)
defer cancel()
// 初始化数据库
db, err := database.InitDefaultDatabase()
if err != nil {
glog.Fatalf("init database failed: %v", err)
}
client, err := dw_sdk.InitDefaultDWClient()
if err != nil {
logrus.Fatalf("初始化redis失败%v", err)
}
// 初始化服务
r := fiber.New(fiber.Config{ErrorHandler: web.ErrHandle})
r.Use(cors.New(), recover.New())
stg := storage.NewStorage(db)
_ = stg.Article().AutoMigrate()
svc := []web.Register{
web.NewProbe(),
service.NewArticle(stg),
service.NewTools(stg, client), // 添加工具服务
}
for _, s := range svc {
s.Registry(r)
}
port := os.Getenv("PORT")
if port == "" {
port = "80"
}
if err = r.Listen(":"+port, fiber.ListenConfig{
EnablePrintRoutes: true,
GracefulContext: ctx,
}); err != nil {
glog.Warningf("service over: %v", err)
}
}

15
go.mod
View File

@ -6,18 +6,25 @@ toolchain go1.24.1
require (
gitea.timerzz.com/kedaya_haitao/common v0.0.0-20250329125718-37b1ee0b6a4b
gitea.timerzz.com/kedaya_haitao/dw-sdk v0.0.0-20240904075121-552ef11ea87c
github.com/gofiber/fiber/v3 v3.0.0-beta.4
github.com/golang/glog v1.2.2
github.com/samber/lo v1.49.1
github.com/sirupsen/logrus v1.9.3
github.com/xuri/excelize/v2 v2.9.0
gorm.io/gorm v1.25.10
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/expr-lang/expr v1.16.9 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofiber/schema v1.2.0 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -25,15 +32,23 @@ require (
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.58.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect
golang.org/x/arch v0.9.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect

46
go.sum
View File

@ -1,7 +1,17 @@
gitea.timerzz.com/kedaya_haitao/common v0.0.0-20250329125718-37b1ee0b6a4b h1:JPa3QxIOsgiKgT66ujH4EiAoIAHxp++Gacf/NpV1xds=
gitea.timerzz.com/kedaya_haitao/common v0.0.0-20250329125718-37b1ee0b6a4b/go.mod h1:cfkwyDHbOjucM8xLLg8yIkZKz33kdVqvBZYrfNjM8oc=
gitea.timerzz.com/kedaya_haitao/dw-sdk v0.0.0-20240904075121-552ef11ea87c h1:pami6fFsRqKYJyJbSGqXaz2hsbgims7/3ma7cY9CwuY=
gitea.timerzz.com/kedaya_haitao/dw-sdk v0.0.0-20240904075121-552ef11ea87c/go.mod h1:Yfl45xbT2dTh4YHqz4E2i++TYq7fahTeSv9JWNFLjNo=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -17,8 +27,11 @@ github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/as
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -33,6 +46,10 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -42,23 +59,39 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
@ -67,18 +100,30 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -89,3 +134,4 @@ gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

View File

@ -1,217 +1,217 @@
package service
import (
"fmt"
"strconv"
"strings"
"gitea.timerzz.com/kedaya_haitao/common/pkg/web"
"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"
"github.com/gofiber/fiber/v3"
"github.com/samber/lo"
"gorm.io/gorm"
)
type Article struct {
storage *storage.Storage
}
func NewArticle(storage *storage.Storage) *Article {
return &Article{
storage: storage,
}
}
func (s *Article) Registry(r fiber.Router) {
api := r.Group("/api/v2")
api.Get("articles", s.List)
api.Get("articles/u/:id", s.Get)
api.Get("articles/dict/brand", s.BrandDict)
api.Patch("articles", s.Update)
api.Get("articles/provider/history/:id", s.ProviderHistory)
api.Get("articles/seller/history/:id", s.SellerHistory)
api.Patch("articles/provider", s.UpdateProviderArticle)
api.Patch("articles/seller", s.UpdateSellerArticle)
//屏蔽相关操作
api.Get("articles/ban", s.ListBanArticle)
api.Post("articles/ban/:ids", s.BanArticle)
api.Delete("articles/ban/:ids", s.LiftBanArticle)
}
func (s *Article) List(c fiber.Ctx) error {
var q storage.FindArticleQuery
if err := c.Bind().Query(&q); err != nil {
return err
}
q.SetBan(false)
var query = storage.NewPageListQuery(&q)
if err := c.Bind().Query(query); err != nil {
return err
}
articles, total, err := s.storage.Article().List(*query, q.SortScope)
if err != nil {
return err
}
return c.JSON(web.NewResponse(web.NewListResponse(total, articles)))
}
func (s *Article) Get(c fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
i, _ := strconv.Atoi(id)
query := storage.NewGetArticleQuery().SetID(uint(i))
article, err := s.storage.Article().Get(query)
if err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
func (s *Article) Update(c fiber.Ctx) error {
var article v2.Article
if err := c.Bind().JSON(&article); err != nil {
return err
}
if article.ID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
if err := s.storage.Article().Update(article); err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
func (s *Article) BrandDict(c fiber.Ctx) error {
var dict = []utils.Dict{
{
Key: string(v2.Brand_Coach),
Title: fmt.Sprintf("蔻驰/%s", v2.Brand_Coach),
Value: v2.Brand_Coach,
},
}
return c.JSON(web.NewResponse(dict))
}
// 获取供应商历史价格
func (s *Article) ProviderHistory(c fiber.Ctx) error {
i := c.Params("id")
id, _ := strconv.Atoi(i)
if id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
prices, err := s.storage.ProviderArticle().ProviderPrice(uint(id))
if err != nil {
return err
}
return c.JSON(web.NewResponse(prices))
}
// 获取销售商历史价格
func (s *Article) SellerHistory(c fiber.Ctx) error {
i := c.Params("id")
id, _ := strconv.Atoi(i)
if id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
prices, err := s.storage.SellerArticle().SellerPrice(uint(id))
if err != nil {
return err
}
return c.JSON(web.NewResponse(prices))
}
func (s *Article) UpdateProviderArticle(c fiber.Ctx) error {
var article v2.ProviderArticle
if err := c.Bind().JSON(&article); err != nil {
return err
}
if err := s.storage.ProviderArticle().Update(article); err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
func (s *Article) UpdateSellerArticle(c fiber.Ctx) error {
var article v2.SellerArticle
if err := c.Bind().JSON(&article); err != nil {
return err
}
if err := s.storage.SellerArticle().Update(article); err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
// BanArticle 禁用商品
func (s *Article) BanArticle(c fiber.Ctx) error {
ids := c.Params("ids")
if ids == "" {
return fiber.NewError(fiber.StatusBadRequest, "ids is required")
}
idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int {
id, _ := strconv.Atoi(item)
return id
})
err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) {
if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": true, "exclude": true}).Error; err != nil {
return err
}
if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error; err != nil {
return err
}
return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error
})
if err != nil {
return err
}
return c.JSON(web.NewResponse("ok"))
}
// 解禁商品
func (s *Article) LiftBanArticle(c fiber.Ctx) error {
ids := c.Params("ids")
if ids == "" {
return fiber.NewError(fiber.StatusBadRequest, "ids is required")
}
idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int {
id, _ := strconv.Atoi(item)
return id
})
err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) {
if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": false, "exclude": false}).Error; err != nil {
return err
}
if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error; err != nil {
return err
}
return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error
})
if err != nil {
return err
}
return c.JSON(web.NewResponse("ok"))
}
func (s *Article) ListBanArticle(c fiber.Ctx) error {
var q storage.FindArticleQuery
if err := c.Bind().Query(&q); err != nil {
return err
}
q.SetBan(true)
var query = storage.NewPageListQuery(&q)
if err := c.Bind().Query(query); err != nil {
return err
}
articles, total, err := s.storage.Article().List(*query, q.SortScope)
if err != nil {
return err
}
return c.JSON(web.NewResponse(web.NewListResponse(total, articles)))
}
package service
import (
"fmt"
"strconv"
"strings"
"gitea.timerzz.com/kedaya_haitao/common/pkg/web"
"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"
"github.com/gofiber/fiber/v3"
"github.com/samber/lo"
"gorm.io/gorm"
)
type Article struct {
storage *storage.Storage
}
func NewArticle(storage *storage.Storage) *Article {
return &Article{
storage: storage,
}
}
func (s *Article) Registry(r fiber.Router) {
api := r.Group("/api/v2")
api.Get("articles", s.List)
api.Get("articles/u/:id", s.Get)
api.Get("articles/dict/brand", s.BrandDict)
api.Patch("articles", s.Update)
api.Get("articles/provider/history/:id", s.ProviderHistory)
api.Get("articles/seller/history/:id", s.SellerHistory)
api.Patch("articles/provider", s.UpdateProviderArticle)
api.Patch("articles/seller", s.UpdateSellerArticle)
//屏蔽相关操作
api.Get("articles/ban", s.ListBanArticle)
api.Post("articles/ban/:ids", s.BanArticle)
api.Delete("articles/ban/:ids", s.LiftBanArticle)
}
func (s *Article) List(c fiber.Ctx) error {
var q storage.FindArticleQuery
if err := c.Bind().Query(&q); err != nil {
return err
}
q.SetBan(false)
var query = storage.NewPageListQuery(&q)
if err := c.Bind().Query(query); err != nil {
return err
}
articles, total, err := s.storage.Article().List(*query, q.SortScope)
if err != nil {
return err
}
return c.JSON(web.NewResponse(web.NewListResponse(total, articles)))
}
func (s *Article) Get(c fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
i, _ := strconv.Atoi(id)
query := storage.NewGetArticleQuery().SetID(uint(i))
article, err := s.storage.Article().Get(query)
if err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
func (s *Article) Update(c fiber.Ctx) error {
var article v2.Article
if err := c.Bind().JSON(&article); err != nil {
return err
}
if article.ID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
if err := s.storage.Article().Update(article); err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
func (s *Article) BrandDict(c fiber.Ctx) error {
var dict = []utils.Dict{
{
Key: string(v2.Brand_Coach),
Title: fmt.Sprintf("蔻驰/%s", v2.Brand_Coach),
Value: v2.Brand_Coach,
},
}
return c.JSON(web.NewResponse(dict))
}
// 获取供应商历史价格
func (s *Article) ProviderHistory(c fiber.Ctx) error {
i := c.Params("id")
id, _ := strconv.Atoi(i)
if id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
prices, err := s.storage.ProviderArticle().ProviderPrice(uint(id))
if err != nil {
return err
}
return c.JSON(web.NewResponse(prices))
}
// 获取销售商历史价格
func (s *Article) SellerHistory(c fiber.Ctx) error {
i := c.Params("id")
id, _ := strconv.Atoi(i)
if id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "id is required")
}
prices, err := s.storage.SellerArticle().SellerPrice(uint(id))
if err != nil {
return err
}
return c.JSON(web.NewResponse(prices))
}
func (s *Article) UpdateProviderArticle(c fiber.Ctx) error {
var article v2.ProviderArticle
if err := c.Bind().JSON(&article); err != nil {
return err
}
if err := s.storage.ProviderArticle().Update(article); err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
func (s *Article) UpdateSellerArticle(c fiber.Ctx) error {
var article v2.SellerArticle
if err := c.Bind().JSON(&article); err != nil {
return err
}
if err := s.storage.SellerArticle().Update(article); err != nil {
return err
}
return c.JSON(web.NewResponse(article))
}
// BanArticle 禁用商品
func (s *Article) BanArticle(c fiber.Ctx) error {
ids := c.Params("ids")
if ids == "" {
return fiber.NewError(fiber.StatusBadRequest, "ids is required")
}
idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int {
id, _ := strconv.Atoi(item)
return id
})
err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) {
if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": true, "exclude": true}).Error; err != nil {
return err
}
if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error; err != nil {
return err
}
return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", true).Error
})
if err != nil {
return err
}
return c.JSON(web.NewResponse("ok"))
}
// 解禁商品
func (s *Article) LiftBanArticle(c fiber.Ctx) error {
ids := c.Params("ids")
if ids == "" {
return fiber.NewError(fiber.StatusBadRequest, "ids is required")
}
idList := lo.Map(strings.Split(ids, ","), func(item string, index int) int {
id, _ := strconv.Atoi(item)
return id
})
err := s.storage.DB().Transaction(func(tx *gorm.DB) (err error) {
if err = tx.Model(&v2.Article{}).Select("ban", "exclude").Where("id IN ?", idList).Updates(map[string]interface{}{"ban": false, "exclude": false}).Error; err != nil {
return err
}
if err = tx.Model(&v2.ProviderArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error; err != nil {
return err
}
return tx.Model(&v2.SellerArticle{}).Where("article_id IN ?", idList).Update("exclude", false).Error
})
if err != nil {
return err
}
return c.JSON(web.NewResponse("ok"))
}
func (s *Article) ListBanArticle(c fiber.Ctx) error {
var q storage.FindArticleQuery
if err := c.Bind().Query(&q); err != nil {
return err
}
q.SetBan(true)
var query = storage.NewPageListQuery(&q)
if err := c.Bind().Query(query); err != nil {
return err
}
articles, total, err := s.storage.Article().List(*query, q.SortScope)
if err != nil {
return err
}
return c.JSON(web.NewResponse(web.NewListResponse(total, articles)))
}

72
service/tools_svc.go Normal file
View File

@ -0,0 +1,72 @@
package service
import (
"fmt"
coach_outlet "gitea.timerzz.com/kedaya_haitao/article/tools/coach-outlet"
"gitea.timerzz.com/kedaya_haitao/common/structs/storage"
dw_sdk "gitea.timerzz.com/kedaya_haitao/dw-sdk"
"github.com/gofiber/fiber/v3"
"github.com/sirupsen/logrus"
)
type Tools struct {
storage *storage.Storage
dw *dw_sdk.Client
export *coach_outlet.Exporter
}
// NewTools 创建工具服务实例
func NewTools(storage *storage.Storage, dw *dw_sdk.Client) *Tools {
exporter, err := coach_outlet.NewExporter(storage, dw)
if err != nil {
logrus.Errorf("初始化Coach Outlet导出工具失败: %v", err)
}
return &Tools{
storage: storage,
dw: dw,
export: exporter,
}
}
// Registry 注册路由
func (s *Tools) Registry(r fiber.Router) {
api := r.Group("/api/v2")
// 添加新的Coach Outlet工具接口
tools := api.Group("/tools")
tools.Get("/excel/coach-outlet/:provider_id", s.CoachOutletExcel)
}
// CoachOutletExcel 导出Coach Outlet商品数据到Excel
func (s *Tools) CoachOutletExcel(c fiber.Ctx) error {
// 获取供应商ID参数
providerId := c.Params("provider_id")
if providerId == "" {
providerId = coach_outlet.CACoachOutlet // 默认使用加拿大Coach Outlet
}
// 验证供应商ID是否有效
if providerId != coach_outlet.CACoachOutlet && providerId != coach_outlet.USCoachOutlet {
return fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("无效的供应商ID必须是 %s 或 %s", coach_outlet.CACoachOutlet, coach_outlet.USCoachOutlet))
}
// 检查导出工具是否初始化成功
if s.export == nil {
return fiber.NewError(fiber.StatusInternalServerError, "导出工具未初始化")
}
// 设置文件名和响应头
fileName := fmt.Sprintf("coach_outlet_%s.xlsx", providerId)
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, fileName))
c.Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
// 直接将Excel数据写入响应
if err := s.export.ExportCheapProduct(providerId, c); err != nil {
logrus.Errorf("导出Coach Outlet商品数据失败: %v", err)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("导出失败: %v", err))
}
return nil
}

294
tools/coach-outlet/excel.go Normal file
View File

@ -0,0 +1,294 @@
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
}