From 5c840501dc50caae70f8ee5966a7aea6358ea27a Mon Sep 17 00:00:00 2001 From: Kevin Lee <363344+ipfans@users.noreply.github.com> Date: Mon, 25 Apr 2022 12:07:39 +0800 Subject: [PATCH] feat: Product API --- README.md | 34 ++-- product.go | 124 +++++++++++++ product_test.go | 237 ++++++++++++++++++++++++ request.go | 10 + response.go | 65 +++++++ testdata/product/get_attribute.json | 75 ++++++++ testdata/product/get_brand.json | 61 ++++++ testdata/product/get_category.json | 82 ++++++++ testdata/product/get_category_rule.json | 71 +++++++ testdata/product/upload_file.json | 44 +++++ testdata/product/upload_img.json | 45 +++++ 11 files changed, 831 insertions(+), 17 deletions(-) create mode 100644 product.go create mode 100644 product_test.go create mode 100644 testdata/product/get_attribute.json create mode 100644 testdata/product/get_brand.json create mode 100644 testdata/product/get_category.json create mode 100644 testdata/product/get_category_rule.json create mode 100644 testdata/product/upload_file.json create mode 100644 testdata/product/upload_img.json diff --git a/README.md b/README.md index bf2abd2..889b653 100644 --- a/README.md +++ b/README.md @@ -37,23 +37,23 @@ Go SDK for Tiktok Shop Open Platform. - [ ] GetShippingDocument - [ ] GetWarehouseList - [ ] GetShippingProvider -- [ ] Product API - - [ ] GetCategory - - [ ] GetAttribute - - [ ] GetCategoryRule - - [ ] GetBrand - - [ ] UploadImg - - [ ] UploadFile - - [ ] CreateProduct - - [ ] EditProduct - - [ ] GetProductList - - [ ] GetProductDetail - - [ ] UpdatePrice - - [ ] UpdateStock - - [ ] DeactivateProducts - - [ ] DeleteProducts - - [ ] RecoverProduct - - [ ] ActivateProduct +- [x] Product API + - [x] GetCategory + - [x] GetAttribute + - [x] GetCategoryRule + - [x] GetBrand + - [x] UploadImg + - [x] UploadFile + - [x] CreateProduct + - [x] EditProduct + - [x] GetProductList + - [x] GetProductDetail + - [x] UpdatePrice + - [x] UpdateStock + - [x] DeactivateProducts + - [x] DeleteProducts + - [x] RecoverProduct + - [x] ActivateProduct - [x] Shop API - [x] GetAuthorizedShop - [x] Finance API diff --git a/product.go b/product.go new file mode 100644 index 0000000..d199e1e --- /dev/null +++ b/product.go @@ -0,0 +1,124 @@ +package tiktok + +import ( + "context" + "encoding/base64" + "io" + "net/url" +) + +// GetCategory +// WARN: DO NOT CACHE THIS FUNCTION RESULT. +func (c *Client) GetCategory(ctx context.Context, p Param) (list CategoryList, err error) { + var param url.Values + if param, err = c.params(p); err != nil { + return + } + err = c.Get(ctx, "/api/products/categories", param, &list) + return +} + +func (c *Client) GetAttribute(ctx context.Context, p Param, categoryID string) (list AttributeList, err error) { + var param url.Values + if param, err = c.params(p); err != nil { + return + } + param.Set("category_id", categoryID) + err = c.Get(ctx, "/api/products/attributes", param, &list) + return +} + +func (c *Client) GetCategoryRule(ctx context.Context, p Param, categoryID string) (list CategoryRules, err error) { + var param url.Values + if param, err = c.params(p); err != nil { + return + } + param.Set("category_id", categoryID) + err = c.Get(ctx, "/api/products/categories/rules", param, &list) + return +} + +func (c *Client) GetBrand(ctx context.Context, p Param) (list BrandList, err error) { + var param url.Values + if param, err = c.params(p); err != nil { + return + } + err = c.Get(ctx, "/api/products/brands", param, &list) + return +} + +func (c *Client) UploadImgReader(ctx context.Context, p Param, scene ImgScene, r io.Reader) (img ImageInfo, err error) { + var b []byte + if b, err = io.ReadAll(r); err != nil { + return + } + body := base64.StdEncoding.EncodeToString(b) + img, err = c.UploadImg(ctx, p, scene, body) + return +} + +func (c *Client) UploadImg(ctx context.Context, p Param, scene ImgScene, body string) (img ImageInfo, err error) { + var param url.Values + if param, err = c.params(p); err != nil { + return + } + m := map[string]interface{}{ + "img_data": body, + "img_scene": int(scene), + } + err = c.Post(ctx, "/api/products/upload_imgs", param, m, &img) + return +} + +func (c *Client) UploadFile(ctx context.Context, p Param, fn string, body []byte) (info FileInfo, err error) { + var param url.Values + if param, err = c.params(p); err != nil { + return + } + m := map[string]interface{}{ + "file_name": fn, + "file_data": base64.StdEncoding.EncodeToString(body), + } + err = c.Post(ctx, "/api/products/upload_files", param, m, &info) + return +} + +func (c *Client) CreateProduct(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) EditProduct(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) GetProductList(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) GetProductDetail(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) UpdatePrice(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) UpdateStock(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) DeactivateProducts(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) DeleteProducts(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) RecoverProduct(ctx context.Context, p Param) (err error) { + return +} + +func (c *Client) ActivateProduct(ctx context.Context, p Param) (err error) { + return +} diff --git a/product_test.go b/product_test.go new file mode 100644 index 0000000..8446df7 --- /dev/null +++ b/product_test.go @@ -0,0 +1,237 @@ +package tiktok_test + +import ( + "context" + "strings" + "testing" + + "github.com/ipfans/tiktok" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/require" +) + +func TestClient_GetCategory(t *testing.T) { + var args struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + AccessToken string `json:"access_token"` + ShopID string `json:"shop_id"` + } + + restore := mockTime() + defer restore() + + tests := loadTestData(t, "testdata/product/get_category.json") + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + var want tiktok.CategoryList + setupMock(t, tt, &args, &want) + + c, err := tiktok.New(args.AppKey, args.AppSecret) + require.NoError(t, err) + + list, err := c.GetCategory(context.TODO(), + tiktok.Param{args.AccessToken, args.ShopID}, + ) + if tt.WantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, want, list) + }) + } +} + +func TestClient_GetAttribute(t *testing.T) { + var args struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + AccessToken string `json:"access_token"` + ShopID string `json:"shop_id"` + CategoryID string `json:"category_id"` + } + + restore := mockTime() + defer restore() + + tests := loadTestData(t, "testdata/product/get_attribute.json") + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + var want tiktok.AttributeList + setupMock(t, tt, &args, &want) + + c, err := tiktok.New(args.AppKey, args.AppSecret) + require.NoError(t, err) + + list, err := c.GetAttribute(context.TODO(), + tiktok.Param{args.AccessToken, args.ShopID}, + args.CategoryID, + ) + if tt.WantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, want, list) + }) + } +} + +func TestClient_GetCategoryRule(t *testing.T) { + var args struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + AccessToken string `json:"access_token"` + ShopID string `json:"shop_id"` + CategoryID string `json:"category_id"` + } + + restore := mockTime() + defer restore() + + tests := loadTestData(t, "testdata/product/get_category_rule.json") + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + var want tiktok.CategoryRules + setupMock(t, tt, &args, &want) + + c, err := tiktok.New(args.AppKey, args.AppSecret) + require.NoError(t, err) + + list, err := c.GetCategoryRule(context.TODO(), + tiktok.Param{args.AccessToken, args.ShopID}, + args.CategoryID, + ) + if tt.WantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, want, list) + }) + } +} + +func TestClient_GetBrand(t *testing.T) { + var args struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + AccessToken string `json:"access_token"` + ShopID string `json:"shop_id"` + } + + restore := mockTime() + defer restore() + + tests := loadTestData(t, "testdata/product/get_brand.json") + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + var want tiktok.BrandList + setupMock(t, tt, &args, &want) + + c, err := tiktok.New(args.AppKey, args.AppSecret) + require.NoError(t, err) + + list, err := c.GetBrand(context.TODO(), + tiktok.Param{args.AccessToken, args.ShopID}, + ) + if tt.WantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, want, list) + }) + } +} + +func TestClient_UploadImg(t *testing.T) { + var args struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + AccessToken string `json:"access_token"` + ShopID string `json:"shop_id"` + File string `json:"file"` + } + + restore := mockTime() + defer restore() + + tests := loadTestData(t, "testdata/product/upload_img.json") + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + var want tiktok.ImageInfo + setupMock(t, tt, &args, &want) + + c, err := tiktok.New(args.AppKey, args.AppSecret) + require.NoError(t, err) + r := strings.NewReader(args.File) + + img, err := c.UploadImgReader(context.TODO(), + tiktok.Param{args.AccessToken, args.ShopID}, + tiktok.ImgSceneAttributeImage, r, + ) + if tt.WantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, want, img) + }) + } +} + +func TestClient_UploadFile(t *testing.T) { + var args struct { + AppKey string `json:"app_key"` + AppSecret string `json:"app_secret"` + AccessToken string `json:"access_token"` + ShopID string `json:"shop_id"` + File string `json:"file"` + Name string `json:"name"` + } + + restore := mockTime() + defer restore() + + tests := loadTestData(t, "testdata/product/upload_file.json") + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + var want tiktok.FileInfo + setupMock(t, tt, &args, &want) + + c, err := tiktok.New(args.AppKey, args.AppSecret) + require.NoError(t, err) + + info, err := c.UploadFile(context.TODO(), + tiktok.Param{args.AccessToken, args.ShopID}, + args.Name, []byte(args.File), + ) + if tt.WantErr { + require.Error(t, err) + return + } else { + require.NoError(t, err) + } + require.Equal(t, want, info) + }) + } +} diff --git a/request.go b/request.go index d1c3b51..3941a23 100644 --- a/request.go +++ b/request.go @@ -80,3 +80,13 @@ type GetReverseReasonRequest struct { ReverseActionType int `json:"reverse_action_type"` ReasonType int `json:"reason_type"` } + +type ImgScene int + +const ( + ImgSceneProductImage ImgScene = iota + 1 + ImgSceneDescriptionImage + ImgSceneAttributeImage + ImgSceneCertificationImage + ImgSceneSizeChartImage +) diff --git a/response.go b/response.go index 769f281..3256b04 100644 --- a/response.go +++ b/response.go @@ -204,3 +204,68 @@ type ReverseReason struct { type ReverseReasonList struct { ReverseReasonList []ReverseReason `json:"reverse_reason_list"` } + +type Category struct { + ID string `json:"id"` + ParentID string `json:"parent_id"` + LocalDisplayName string `json:"local_display_name"` + IsLeaf bool `json:"is_leaf"` +} +type CategoryList struct { + CategoryList []Category `json:"category_list"` +} + +type InputType struct { + IsMandatory bool `json:"is_mandatory"` + IsMultipleSelected bool `json:"is_multiple_selected"` + IsCustomized bool `json:"is_customized"` +} + +type Attribute struct { + ID string `json:"id"` + Name string `json:"name"` + AttributeType int `json:"attribute_type"` + InputType InputType `json:"input_type"` +} +type AttributeList struct { + Attributes []Attribute `json:"attributes"` +} + +type ProductCertification struct { + Name string `json:"certification_name"` + ID string `json:"certification_id"` + Sample string `json:"certification_sample"` + IsMandatory bool `json:"is_mandatory"` +} +type CategoryRule struct { + ProductCertifications []ProductCertification `json:"product_certifications"` + SupportSizeChart bool `json:"support_size_chart"` + SupportCod bool `json:"support_cod"` +} + +type CategoryRules struct { + CategoryRules []CategoryRule `json:"category_rules"` +} + +type Brand struct { + ID string `json:"id"` + Name string `json:"name"` +} +type BrandList struct { + BrandList []Brand `json:"brand_list"` +} + +type ImageInfo struct { + ImgID string `json:"img_id"` + ImgURL string `json:"img_url"` + ImgHeight int `json:"img_height"` + ImgWidth int `json:"img_width"` + ImgScene int `json:"img_scene"` +} + +type FileInfo struct { + FileID string `json:"file_id"` + FileURL string `json:"file_url"` + FileName string `json:"file_name"` + FileType string `json:"file_type"` +} diff --git a/testdata/product/get_attribute.json b/testdata/product/get_attribute.json new file mode 100644 index 0000000..00e38dc --- /dev/null +++ b/testdata/product/get_attribute.json @@ -0,0 +1,75 @@ +[ + { + "name": "Official example", + "args": { + "app_key": "12abcd", + "app_secret": "123", + "access_token": "abc1c123-3128-aa17-125e-2d2d51ab814fa", + "shop_id": "123456", + "category_id": "1234" + }, + "request": { + "method": "GET", + "url": "https://open-api.tiktokglobalshop.com/api/products/attributes", + "headers": {}, + "query": "access_token=abc1c123-3128-aa17-125e-2d2d51ab814fa&app_key=12abcd&category_id=1234&shop_id=123456&sign=bdbcdf52d2a1b99bf6187aaa181e8cff8b4f4cd0d3919e264aef5d3d83801732×tamp=1600000000" + }, + "response": { + "status": 200, + "body": { + "code": 0, + "message": "Success", + "request_id": "1111111", + "data": { + "attributes": [ + { + "id": "111", + "name": "aaa", + "attribute_type": 1, + "input_type": { + "is_mandatory": true, + "is_multiple_selected": true, + "is_customized": true + } + }, + { + "id": "222", + "name": "bbb", + "attribute_type": 2, + "input_type": { + "is_mandatory": true, + "is_multiple_selected": true, + "is_customized": true + } + } + ] + } + } + }, + "want": { + "attributes": [ + { + "id": "111", + "name": "aaa", + "attribute_type": 1, + "input_type": { + "is_mandatory": true, + "is_multiple_selected": true, + "is_customized": true + } + }, + { + "id": "222", + "name": "bbb", + "attribute_type": 2, + "input_type": { + "is_mandatory": true, + "is_multiple_selected": true, + "is_customized": true + } + } + ] + }, + "want_err": false + } +] \ No newline at end of file diff --git a/testdata/product/get_brand.json b/testdata/product/get_brand.json new file mode 100644 index 0000000..b588412 --- /dev/null +++ b/testdata/product/get_brand.json @@ -0,0 +1,61 @@ +[ + { + "name": "Official example", + "args": { + "app_key": "12abcd", + "app_secret": "123", + "access_token": "abc1c123-3128-aa17-125e-2d2d51ab814fa", + "shop_id": "123456", + "category_id": "1234" + }, + "request": { + "method": "GET", + "url": "https://open-api.tiktokglobalshop.com/api/products/brands", + "headers": {}, + "query": "access_token=abc1c123-3128-aa17-125e-2d2d51ab814fa&app_key=12abcd&shop_id=123456&sign=864136f2b17f500075ee4a82a96cf930cbedb2e1360f5d9641d98c97e5e5797c×tamp=1600000000" + }, + "response": { + "status": 200, + "body": { + "code": 0, + "message": "Success", + "request_id": "111111", + "data": { + "code": 0, + "message": "Success", + "request_id": "111111111", + "data": { + "brand_list": [ + { + "id": "1111111", + "name": "aaa" + }, + { + "id": "2222222", + "name": "bbb" + } + ] + } + } + } + }, + "want": { + "code": 0, + "message": "Success", + "request_id": "111111111", + "data": { + "brand_list": [ + { + "id": "1111111", + "name": "aaa" + }, + { + "id": "2222222", + "name": "bbb" + } + ] + } + }, + "want_err": false + } +] \ No newline at end of file diff --git a/testdata/product/get_category.json b/testdata/product/get_category.json new file mode 100644 index 0000000..91e268e --- /dev/null +++ b/testdata/product/get_category.json @@ -0,0 +1,82 @@ +[ + { + "name": "Official example", + "args": { + "app_key": "12abcd", + "app_secret": "123", + "access_token": "abc1c123-3128-aa17-125e-2d2d51ab814fa", + "shop_id": "123456" + }, + "request": { + "method": "GET", + "url": "https://open-api.tiktokglobalshop.com/api/products/categories", + "headers": {}, + "query": "access_token=abc1c123-3128-aa17-125e-2d2d51ab814fa&app_key=12abcd&shop_id=123456&sign=5c173f5aaafa741dda621ffb2189901d74259930f1fca533ff2cfe84c7a77148×tamp=1600000000" + }, + "response": { + "status": 200, + "body": { + "code": 0, + "message": "Success", + "request_id": "11111", + "data": { + "category_list": [ + { + "id": "1", + "parent_id": "1111", + "local_display_name": "aaaa", + "is_leaf": false + }, + { + "id": "2", + "parent_id": "2222", + "local_display_name": "bbbb", + "is_leaf": false + }, + { + "id": "3", + "parent_id": "3333", + "local_display_name": "cccc", + "is_leaf": false + }, + { + "id": "4", + "parent_id": "4444", + "local_display_name": "dddd", + "is_leaf": true + } + ] + } + } + }, + "want": { + "category_list": [ + { + "id": "1", + "parent_id": "1111", + "local_display_name": "aaaa", + "is_leaf": false + }, + { + "id": "2", + "parent_id": "2222", + "local_display_name": "bbbb", + "is_leaf": false + }, + { + "id": "3", + "parent_id": "3333", + "local_display_name": "cccc", + "is_leaf": false + }, + { + "id": "4", + "parent_id": "4444", + "local_display_name": "dddd", + "is_leaf": true + } + ] + }, + "want_err": false + } +] \ No newline at end of file diff --git a/testdata/product/get_category_rule.json b/testdata/product/get_category_rule.json new file mode 100644 index 0000000..07f6948 --- /dev/null +++ b/testdata/product/get_category_rule.json @@ -0,0 +1,71 @@ +[ + { + "name": "Official example", + "args": { + "app_key": "12abcd", + "app_secret": "123", + "access_token": "abc1c123-3128-aa17-125e-2d2d51ab814fa", + "shop_id": "123456", + "category_id": "1234" + }, + "request": { + "method": "GET", + "url": "https://open-api.tiktokglobalshop.com/api/products/categories/rules", + "headers": {}, + "query": "access_token=abc1c123-3128-aa17-125e-2d2d51ab814fa&app_key=12abcd&category_id=1234&shop_id=123456&sign=220a13779c3760007c1d7c04ca0c4fe9f4b870719908d40f2d3fca7bac75d02f×tamp=1600000000" + }, + "response": { + "status": 200, + "body": { + "code": 0, + "message": "Success", + "request_id": "111111", + "data": { + "category_rules": [ + { + "product_certifications": [ + { + "certification_name": "aaaa", + "certification_id": "11", + "certification_sample": "aaaaaaaaa", + "is_mandatory": false + }, + { + "certification_name": "bbbb", + "certification_id": "22", + "certification_sample": "bbbbbbbbb", + "is_mandatory": false + } + ], + "support_size_chart": false, + "support_cod": true + } + ] + } + } + }, + "want": { + "category_rules": [ + { + "product_certifications": [ + { + "certification_name": "aaaa", + "certification_id": "11", + "certification_sample": "aaaaaaaaa", + "is_mandatory": false + }, + { + "certification_name": "bbbb", + "certification_id": "22", + "certification_sample": "bbbbbbbbb", + "is_mandatory": false + } + ], + "support_size_chart": false, + "support_cod": true + } + ] + }, + "want_err": false + } +] \ No newline at end of file diff --git a/testdata/product/upload_file.json b/testdata/product/upload_file.json new file mode 100644 index 0000000..0b05e7d --- /dev/null +++ b/testdata/product/upload_file.json @@ -0,0 +1,44 @@ +[ + { + "name": "Official example", + "args": { + "app_key": "12abcd", + "app_secret": "123", + "access_token": "abc1c123-3128-aa17-125e-2d2d51ab814fa", + "shop_id": "123456", + "file": "testdata/refresh_token.json", + "name": "test.pdf" + }, + "request": { + "method": "POST", + "url": "https://open-api.tiktokglobalshop.com/api/products/upload_files", + "headers": {}, + "query": "access_token=abc1c123-3128-aa17-125e-2d2d51ab814fa&app_key=12abcd&shop_id=123456&sign=f8c097c3adc46c48f9eb50cf5afb91682af5aab910a9820e67686f8265e96550×tamp=1600000000", + "body": { + "file_data": "dGVzdGRhdGEvcmVmcmVzaF90b2tlbi5qc29u", + "file_name": "test.pdf" + } + }, + "response": { + "status": 200, + "body": { + "code": 0, + "message": "Success", + "request_id": "123333111131313131", + "data": { + "file_id": "aaaaaa", + "file_url": "http://bbbbbbbb", + "file_name": "file1.pdf", + "file_type": "PDF" + } + } + }, + "want": { + "file_id": "aaaaaa", + "file_url": "http://bbbbbbbb", + "file_name": "file1.pdf", + "file_type": "PDF" + }, + "want_err": false + } +] \ No newline at end of file diff --git a/testdata/product/upload_img.json b/testdata/product/upload_img.json new file mode 100644 index 0000000..c3e5b6a --- /dev/null +++ b/testdata/product/upload_img.json @@ -0,0 +1,45 @@ +[ + { + "name": "Official example", + "args": { + "app_key": "12abcd", + "app_secret": "123", + "access_token": "abc1c123-3128-aa17-125e-2d2d51ab814fa", + "shop_id": "123456", + "file": "testdata/refresh_token.json" + }, + "request": { + "method": "POST", + "url": "https://open-api.tiktokglobalshop.com/api/products/upload_imgs", + "headers": {}, + "query": "access_token=abc1c123-3128-aa17-125e-2d2d51ab814fa&app_key=12abcd&shop_id=123456&sign=6ad527ffdc248aa67c4947b2a593086fa65f37f0d4b83bde705d67f823d62a4f×tamp=1600000000", + "body": { + "img_data": "dGVzdGRhdGEvcmVmcmVzaF90b2tlbi5qc29u", + "img_scene": 3 + } + }, + "response": { + "status": 200, + "body": { + "code": 0, + "message": "Success", + "request_id": "1111111111", + "data": { + "img_id": "aaaaabbbbb", + "img_url": "http://bbbbbbbb", + "img_height": 1000, + "img_width": 1000, + "img_scene": 1 + } + } + }, + "want": { + "img_id": "aaaaabbbbb", + "img_url": "http://bbbbbbbb", + "img_height": 1000, + "img_width": 1000, + "img_scene": 1 + }, + "want_err": false + } +] \ No newline at end of file