From 20a3b65f017be4c7098b86a33d576f69cb86150f Mon Sep 17 00:00:00 2001 From: lzh <18320341470> Date: Wed, 28 May 2025 09:59:12 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=93=81=E7=9B=B8=E5=85=B3=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 7 + go.sum | 9 ++ weipinshang/goods_api.go | 107 +++++++++++++ weipinshang/goods_api_test.go | 112 ++++++++++++++ weipinshang/model.go | 201 +++++++++++++++++++++++++ weipinshang/wei_pin_shang_client.go | 223 ++++++++++++++++++++++++++++ 6 files changed, 659 insertions(+) create mode 100644 weipinshang/goods_api.go create mode 100644 weipinshang/goods_api_test.go create mode 100644 weipinshang/model.go create mode 100644 weipinshang/wei_pin_shang_client.go diff --git a/go.mod b/go.mod index 02a2f8d..81fbde1 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,10 @@ module git.ssgfgtfy.com/public/ssgf_utils go 1.18.0 require github.com/pkg/errors v0.9.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 7c401c3..ba9f6d0 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,11 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/weipinshang/goods_api.go b/weipinshang/goods_api.go new file mode 100644 index 0000000..756e578 --- /dev/null +++ b/weipinshang/goods_api.go @@ -0,0 +1,107 @@ +package wps + +import "fmt" + +// GetGoodBrand 查询商品的品牌 +func (c *WeiPinShangClient) GetGoodBrand(req GoodBrandReq) (*GoodBrandList, error) { + param := StructToMapString(req) + out := GoodBrandRes{} + err := c.PostAndParse(GetGoodBrandURL, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} + +// GetGoodsClassify 查询商品分类 +func (c *WeiPinShangClient) GetGoodsClassify(req GetGoodsClassifyReq) (*[]GoodsClassify, error) { + param := StructToMapString(req) + out := GetGoodsClassifyRes{} + err := c.PostAndParse(GetGoodsClassifyURL, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} + +// GetGoodsList 获取全部商品列表接口 +func (c *WeiPinShangClient) GetGoodsList(req GetGoodsListReq) (*GetGoodsList, error) { + param := StructToMapString(req) + out := GetGoodsListRes{} + err := c.PostAndParse(GetGoodsListURL, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} + +// GetGoodsdept (推荐使用)获取后台已选商品列表接口 +func (c *WeiPinShangClient) GetGoodsdept(req GetGoodsdeptReq) (*Goodsdept, error) { + param := StructToMapString(req) + out := GetGoodsdeptRes{} + err := c.PostAndParse(GetGoodsdeptURL, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} + +// GetDetailsGoods 获取批量商品详情 +func (c *WeiPinShangClient) GetDetailsGoods(req GetDetailsGoodsReq) (*[]GoodsItem, error) { + param := StructToMapString(req) + out := GetDetailsGoodsRes{} + err := c.PostAndParse(GetDetailsGoodsUrl, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} + +// GetGoodsDetails 获取单个商品详情接口 +func (c *WeiPinShangClient) GetGoodsDetails(req GetGoodsDetailsReq) (*GoodsItem, error) { + param := StructToMapString(req) + out := GetGoodsDetailsRes{} + err := c.PostAndParse(GetGoodsDetailsURL, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} + +func (c *WeiPinShangClient) GetGoodsStock(req GetGoodsStockReq) (*[]GoodsStock, error) { + param := StructToMapString(req) + out := GetGoodsStockRes{} + err := c.PostAndParse(GetGoodsStockURL, param, &out) + if err != nil { + return nil, err + } + if out.Code != 0 { + return nil, fmt.Errorf(out.Msg) + } + res := out.Data + return &res, err +} diff --git a/weipinshang/goods_api_test.go b/weipinshang/goods_api_test.go new file mode 100644 index 0000000..401985a --- /dev/null +++ b/weipinshang/goods_api_test.go @@ -0,0 +1,112 @@ +package wps_test + +import ( + wps "git.ssgfgtfy.com/public/ssgf_utils/weipinshang" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func setupMockServer(path string, method string, response string, t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/"+path, r.URL.Path) + assert.Equal(t, method, r.Method) + w.WriteHeader(http.StatusOK) + w.Write([]byte(response)) + })) +} + +func newClientWithServer(ts *httptest.Server) *wps.WeiPinShangClient { + return wps.NewWeiPinShangClient(wps.Config{ + BaseURL: wps.DevHost, + ChannelType: "AILEHUI", + Key: "f654ea5bde7635c3f46191191e5c4c8e", + }) +} + +func TestGetGoodBrand(t *testing.T) { + ts := setupMockServer(wps.GetGoodBrandURL, http.MethodPost, `{"code":200,"data":{"brands":[]}}`, t) + defer ts.Close() + client := newClientWithServer(ts) + res, err := client.GetGoodBrand(wps.GoodBrandReq{ + PageNo: "2", + PageSize: "2", + CBrandName: "测试", + }) + assert.NoError(t, err) + assert.NotNil(t, res) +} + +func TestGetGoodsClassify(t *testing.T) { + ts := setupMockServer(wps.GetGoodsClassifyURL, http.MethodPost, `{"code":200,"data":{"classify":[]}}`, t) + defer ts.Close() + + client := newClientWithServer(ts) + res, err := client.GetGoodsClassify(wps.GetGoodsClassifyReq{ + CLevel: "2", + CParentCode: "13315", + }) + assert.NoError(t, err) + assert.NotNil(t, res) +} + +func TestGetGoodsList(t *testing.T) { + ts := setupMockServer(wps.GetGoodsListURL, http.MethodPost, `{"code":200,"data":{"goods":[]}}`, t) + defer ts.Close() + + client := newClientWithServer(ts) + res, err := client.GetGoodsList(wps.GetGoodsListReq{ + PageNo: "1", + PageSize: "10", + }) + assert.NoError(t, err) + assert.NotNil(t, res) +} + +func TestGetGoodsdept(t *testing.T) { + ts := setupMockServer(wps.GetGoodsdeptURL, http.MethodPost, `{"code":200,"data":{"list":[]}}`, t) + defer ts.Close() + + client := newClientWithServer(ts) + res, err := client.GetGoodsdept(wps.GetGoodsdeptReq{}) + assert.NoError(t, err) + assert.NotNil(t, res) +} + +func TestGetDetailsGoods(t *testing.T) { + ts := setupMockServer(wps.GetDetailsGoodsUrl, http.MethodPost, `{"code":200,"data":{"details":[]}}`, t) + defer ts.Close() + + client := newClientWithServer(ts) + res, err := client.GetDetailsGoods(wps.GetDetailsGoodsReq{ + FatherId: "WPS9_282520,WPS9_282450", + }) + assert.NoError(t, err) + assert.NotNil(t, res) +} + +func TestGetGoodsDetails(t *testing.T) { + ts := setupMockServer(wps.GetGoodsDetailsURL, http.MethodPost, `{"code":200,"data":{"goodsInfo":{}}}`, t) + defer ts.Close() + + client := newClientWithServer(ts) + res, err := client.GetGoodsDetails(wps.GetGoodsDetailsReq{ + FatherId: "WPS9_282520", + }) + assert.NoError(t, err) + assert.NotNil(t, res) +} + +func TestGetGoodsStock(t *testing.T) { + ts := setupMockServer(wps.GetGoodsStockURL, http.MethodPost, `{"code":200,"data":{"stocks":[]}}`, t) + defer ts.Close() + + client := newClientWithServer(ts) + res, err := client.GetGoodsStock(wps.GetGoodsStockReq{ + FatherId: "WPS9_282520", + }) + assert.NoError(t, err) + assert.NotNil(t, res) +} diff --git a/weipinshang/model.go b/weipinshang/model.go new file mode 100644 index 0000000..0919591 --- /dev/null +++ b/weipinshang/model.go @@ -0,0 +1,201 @@ +package wps + +//type BaseRes struct { +// Code int64 `json:"code"` //返回编码[0为成功,其它为失败] +// Msg string `json:"msg"` //返回信息[请求接口消息] +// Data interface{} `json:"data"` //返回数据 +//} + +// GoodBrandReq 查询商品的品牌 +type GoodBrandReq struct { + PageNo string `json:"pageNo"` //页码 + PageSize string `json:"pageSize"` //条数 + CBrandName string `json:"c_brand_name"` //品牌名称 模糊查询 +} + +type GoodBrandRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data GoodBrandList `json:"data"` //返回数据 +} + +type GoodBrandList struct { + PageIndex interface{} `json:"pageIndex"` //有时是字符串,有时是int + PageCount int `json:"pageCount"` + DataCount int `json:"dataCount"` + List []GoodBrand `json:"list"` +} + +type GoodBrand struct { + CId int `json:"c_id"` //自增id + CBrandName string `json:"c_brand_name"` //名称 + CBrandLogoUrl string `json:"c_brand_logo_url"` //logo地址 + CCreateTime string `json:"c_create_time"` + CUpdateTime string `json:"c_update_time"` +} + +// GetGoodsClassifyReq 查询商品分类 +type GetGoodsClassifyReq struct { + CLevel string `json:"c_level"` //分类层级,1为一级分类 2为二级分类 3为三级分类(不传默认为1) + CParentCode string `json:"c_parent_code"` //上级分类code +} + +type GetGoodsClassifyRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data []GoodsClassify `json:"data"` //返回数据 +} + +type GoodsClassify struct { + CId int `json:"c_id"` //自增id + CName string `json:"c_name"` //名称 + CCode string `json:"c_code"` //分类code + CLevel int `json:"c_level"` //层级 + CParentCode string `json:"c_parent_code"` //上级code +} + +// GetGoodsListReq 获取全部商品列表接口(若使用该接口 需跟对接人说明,开启商品消息全推) +type GetGoodsListReq struct { + PageNo string `json:"pageNo"` //页码 [默认查询第1页] + PageSize string `json:"pageSize"` //显示条数 [默认显示10条 最大值100] + IsSort int `json:"is_sort"` //排序 [不传默认为正序,传1为倒序] + ProfitSpace int `json:"profitSpace"` //利润空间 [1=>5%以下,2=>5%-10%,3=>10%-20%,4=>20%-50%,5=>50%-100%,6=>100%-200%,7=>200%-500%,8=>500%-700%,9=>700%-900%,10=>900%以上] +} + +type GetGoodsListRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data GetGoodsList `json:"data"` //返回数据 +} + +type GetGoodsList struct { + PageIndex string `json:"pageIndex"` + PageCount int `json:"pageCount"` + DataCount int `json:"dataCount"` + List []Goods `json:"list"` +} + +type Goods struct { + CId int `json:"c_id"` // 序号(商品ID) + CFatherGoodsId string `json:"c_father_goods_id"` // 商品父类ID + CGoodsName string `json:"c_goods_name"` // 商品名称 + CGoodsImage string `json:"c_goods_image"` // 商品图片(1张商品主图) + CClassId int `json:"c_class_id"` // 分类ID(一级) + CClassDesc string `json:"c_class_desc"` // 分类描述(一级) + CClass2Id int `json:"c_class2_id"` // 二级分类ID + CClass2Desc string `json:"c_class2_desc"` // 二级分类描述 + CBrandId int `json:"c_brand_id"` // 品牌ID + CBrandName string `json:"c_brand_name"` // 品牌名称 + CNoDeliveryArea string `json:"c_no_delivery_area"` // 不发货地区,为 "0" 或 "" 表示不限 + CStartDatetime string `json:"c_start_datetime"` // 上架时间(格式:yyyy-MM-dd HH:mm:ss) + CStopDatetime string `json:"c_stop_datetime"` // 下架时间(格式:yyyy-MM-dd HH:mm:ss) + COriginalPrice string `json:"c_original_price"` // 商品原价(吊牌价) + CSalePrice string `json:"c_sale_price"` // 建议售价 + CInPrice string `json:"c_in_price"` // 进货价格 + CGoodsStockValid int `json:"c_goods_stock_valid"` // 当前有效库存(下单库存,不足时下单失败) + CGoodsStockStart int `json:"c_goods_stock_start"` // 原始库存(商品录入时库存) + CSpecifications string `json:"c_specifications"` // 商品详细规格(商品属性描述) + CCompanyCode string `json:"c_company_code"` // 供应商编码 +} + +// GetGoodsdeptReq (推荐使用)获取后台已选商品列表接口 +type GetGoodsdeptReq struct { + PageNo string `json:"pageNo"` //页码 [默认查询第1页] + PageSize string `json:"pageSize"` //显示条数 [默认显示10条 最大值100] + ProfitSpace int `json:"profitSpace"` //利润空间 [1=>5%以下,2=>5%-10%,3=>10%-20%,4=>20%-50%,5=>50%-100%,6=>100%-200%,7=>200%-500%,8=>500%-700%,9=>700%-900%,10=>900%以上] +} + +type GetGoodsdeptRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data Goodsdept `json:"data"` //返回数据 +} + +type Goodsdept struct { + PageIndex string `json:"pageIndex"` + PageCount int `json:"pageCount"` + DataCount int `json:"dataCount"` + List []Goods `json:"list"` +} + +// GetDetailsGoodsReq 获取批量商品详情接口 +type GetDetailsGoodsReq struct { + FatherId string `json:"father_id"` //商品父级ID Spu编码 +} + +type GetDetailsGoodsRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data []GoodsItem `json:"data"` //返回数据 +} + +type GoodsItem struct { + CExhibitionID string `json:"c_exhibition_id"` // 会场ID(特卖商品时有值) + CFatherGoodsID string `json:"c_father_goods_id"` // 父级商品ID + CGoodsName string `json:"c_goods_name"` // 商品名称 + CGoodsDescription string `json:"c_goods_description"` // 商品描述 + CNoDeliveryArea string `json:"c_no_delivery_area"` // 不发货地区,为"0"或""表示不限 + COriginalPrice string `json:"c_original_price"` // 商品原价(吊牌价) + CInPrice string `json:"c_in_price"` // 商品进价 + CSalePrice string `json:"c_sale_price"` // 商品建议售价 + CGoodsImage string `json:"c_goods_image"` // 商品主图 + CDetailsImages string `json:"c_details_images"` // 商品详情图(多图用“;”分隔) + CBannerImages string `json:"c_banner_images"` // 商品banner图(多图用“;”分隔) + CBrandID int `json:"c_brand_id"` // 品牌ID + CBrandIcon string `json:"c_brand_icon"` // 品牌图标 + CBrandName string `json:"c_brand_name"` // 品牌名称 + CClassID int `json:"c_class_id"` // 一级分类ID + CClassDesc string `json:"c_class_desc"` // 一级分类描述 + CClass2ID int `json:"c_class2_id"` // 二级分类ID + CClass2Desc string `json:"c_class2_desc"` // 二级分类描述 + CClass3ID int `json:"c_class3_id"` // 三级分类ID + CClass3Desc string `json:"c_class3_desc"` // 三级分类描述 + CIsShow int `json:"c_is_show"` // 商品状态:0下架,1上架 + CStartDatetime string `json:"c_start_datetime"` // 上架时间 + CStopDatetime string `json:"c_stop_datetime"` // 下架时间 + CSpecifications string `json:"c_specifications"` // 商品详细规格(商品属性描述) + CCompanyCode string `json:"c_company_code"` // 供应商编码 + GoodsSku []GoodsSkuItem `json:"goods_sku"` // 商品SKU列表 +} + +// 商品SKU信息 +type GoodsSkuItem struct { + CFatherGoodsID string `json:"c_father_goods_id"` // 父级商品ID + CGoodsID string `json:"c_goods_id"` // 商品ID + CGoodsName string `json:"c_goods_name"` // 商品规格名称 + COriginalPrice string `json:"c_original_price"` // 商品市场价 + CInPrice string `json:"c_in_price"` // 商品进价 + CSalePrice string `json:"c_sale_price"` // 商品建议售价 + CGoodsColor string `json:"c_goods_color"` // 商品颜色 + CGoodsSize string `json:"c_goods_size"` // 商品尺寸 + CGoodsImage string `json:"c_goods_image"` // 商品列表图片 + CGoodsStockStart int `json:"c_goods_stock_start"` // 初始总库存 + CGoodsStockValid int `json:"c_goods_stock_valid"` // 有效库存(下单库存参考) + CBuyMinNum int `json:"c_buy_min_num"` // 最小购买数量 + CBuyMaxNum int `json:"c_buy_max_num"` // 最大购买数量 +} + +type GetGoodsDetailsReq struct { + FatherId string `json:"father_id"` //商品父级ID Spu编码 +} + +type GetGoodsDetailsRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data GoodsItem `json:"data"` //返回数据 +} + +type GetGoodsStockReq struct { + FatherId string `json:"father_id"` //商品父级ID Spu编码 +} + +type GetGoodsStockRes struct { + Code int64 `json:"code"` //返回编码[0为成功,其它为失败] + Msg string `json:"msg"` //返回信息[请求接口消息] + Data []GoodsStock `json:"data"` //返回数据 +} + +type GoodsStock struct { + CGoodsId string `json:"c_goods_id"` + CStock int `json:"c_stock"` +} diff --git a/weipinshang/wei_pin_shang_client.go b/weipinshang/wei_pin_shang_client.go new file mode 100644 index 0000000..8632207 --- /dev/null +++ b/weipinshang/wei_pin_shang_client.go @@ -0,0 +1,223 @@ +package wps + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "sort" + "strings" + "time" + + "github.com/pkg/errors" +) + +const ( + DevHost = "https://uat.api.weipinshang.net/" + ReleaseHost = "https://api.weipinshang.net/" +) + +const ( + GetGoodBrandURL = "mcang/Mcang/goodBrand" //查询商品的品牌 + GetGoodsClassifyURL = "mcang/Mcang/getGoodsClassify" //查询商品分类 + GetGoodsListURL = "mcang/Mcang/getGoodsList" //获取全部商品列表接口 + GetGoodsdeptURL = "mcang/Mcang/getGoodsdept" //(推荐使用)获取后台已选商品列表接口 + GetDetailsGoodsUrl = "mcang/Mcang/getDetailsGoods" //获取批量商品详情接口 + GetGoodsDetailsURL = "mcang/Mcang/getGoodsDetails" //获取单个商品详情接口 + GetGoodsStockURL = "mcang/Mcang/getGoodsStock" //获取单个商品详情接口 +) + +// Config 客户端配置 +type Config struct { + ChannelType string + Key string + BaseURL string + Timeout time.Duration +} + +// WeiPinShangClient 客户端结构体 +type WeiPinShangClient struct { + ChannelType string + Key string + BaseURL string + HttpClient *http.Client +} + +// NewWeiPinShangClient 创建客户端 +func NewWeiPinShangClient(cfg Config) *WeiPinShangClient { + if cfg.ChannelType == "" { + cfg.ChannelType = "AILEHUI" + } + if cfg.BaseURL == "" { + cfg.BaseURL = ReleaseHost + } + if cfg.Key == "" { + cfg.Key = "f654ea5bde7635c3f46191191e5c4c8e" + } + if cfg.Timeout == 0 { + cfg.Timeout = 10 * time.Second + } + return &WeiPinShangClient{ + ChannelType: cfg.ChannelType, + Key: cfg.Key, + BaseURL: cfg.BaseURL, + HttpClient: &http.Client{Timeout: cfg.Timeout}, + } +} + +// sign 生成签名 +func (c *WeiPinShangClient) sign(params map[string]string) string { + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + var sb strings.Builder + for _, k := range keys { + v := params[k] + if v == "" { + continue + } + sb.WriteString(k) + sb.WriteString("=") + sb.WriteString(v) + sb.WriteString("&") + } + signStr := strings.TrimRight(sb.String(), "&") + c.ChannelType + c.Key + hash := md5.Sum([]byte(signStr)) + return hex.EncodeToString(hash[:]) +} + +// buildHeaders 构建请求头 +func (c *WeiPinShangClient) buildHeaders(params map[string]string) http.Header { + headers := http.Header{} + headers.Set("channelType", c.ChannelType) + headers.Set("md5", c.sign(params)) + headers.Set("Content-Type", "application/x-www-form-urlencoded") + return headers +} + +// buildRequest 构造请求对象 +func (c *WeiPinShangClient) buildRequest(method, endpoint string, params map[string]string) (*http.Request, error) { + var ( + req *http.Request + err error + ) + + if method == http.MethodPost { + form := url.Values{} + for k, v := range params { + form.Set(k, v) + } + req, err = http.NewRequest(method, c.BaseURL+endpoint, strings.NewReader(form.Encode())) + } else { + query := url.Values{} + for k, v := range params { + query.Set(k, v) + } + req, err = http.NewRequest(method, fmt.Sprintf("%s%s?%s", c.BaseURL, endpoint, query.Encode()), nil) + } + + if err != nil { + return nil, fmt.Errorf("构建请求失败,地址:%s,err:%v", endpoint, err.Error()) + } + req.Header = c.buildHeaders(params) + return req, nil +} + +// doRequest 执行请求并返回字符串响应 +func (c *WeiPinShangClient) doRequest(req *http.Request) (string, error) { + resp, err := c.HttpClient.Do(req) + if err != nil { + return "", errors.Wrap(err, "HTTP request failed") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP status error: %s", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("读取response body失败。err:%v", err.Error()) + } + return string(body), nil +} + +// Post 发起 POST 请求 +func (c *WeiPinShangClient) Post(endpoint string, params map[string]string) (string, error) { + req, err := c.buildRequest(http.MethodPost, endpoint, params) + if err != nil { + return "", err + } + return c.doRequest(req) +} + +// Get 发起 GET 请求 +func (c *WeiPinShangClient) Get(endpoint string, params map[string]string) (string, error) { + req, err := c.buildRequest(http.MethodGet, endpoint, params) + if err != nil { + return "", err + } + return c.doRequest(req) +} + +// GetAndParse 发起 GET 请求并解析 JSON 响应 +func (c *WeiPinShangClient) GetAndParse(endpoint string, params map[string]string, out interface{}) error { + respStr, err := c.Get(endpoint, params) + if err != nil { + return err + } + return json.Unmarshal([]byte(respStr), out) +} + +// PostAndParse 发起 POST 请求并解析 JSON 响应 +func (c *WeiPinShangClient) PostAndParse(endpoint string, params map[string]string, out interface{}) error { + respStr, err := c.Post(endpoint, params) + if err != nil { + return err + } + return json.Unmarshal([]byte(respStr), out) +} + +func StructToMapString(obj interface{}) map[string]string { + result := make(map[string]string) + + v := reflect.ValueOf(obj) + t := reflect.TypeOf(obj) + + // 支持结构体指针 + if v.Kind() == reflect.Ptr { + v = v.Elem() + t = t.Elem() + } + + for i := 0; i < v.NumField(); i++ { + field := t.Field(i) + valueField := v.Field(i) + + // 只导出可访问的字段 + if !valueField.CanInterface() { + continue + } + + // 判断是否为零值 + if reflect.DeepEqual(valueField.Interface(), reflect.Zero(valueField.Type()).Interface()) { + continue + } + + key := field.Tag.Get("json") + if key == "" { + key = field.Name + } + + result[key] = fmt.Sprintf("%v", valueField.Interface()) + } + + return result +}