feat: 1.新增多个货易通工具 2.新增货易通创建商品工作流
This commit is contained in:
parent
8d4f3c494e
commit
208f749483
|
|
@ -88,6 +88,23 @@ eino_tools:
|
||||||
# 货易通仓库查询
|
# 货易通仓库查询
|
||||||
hytWarehouseSearch:
|
hytWarehouseSearch:
|
||||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/warehouse/list"
|
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/warehouse/list"
|
||||||
|
# 货易通商品添加
|
||||||
|
hytGoodsAdd:
|
||||||
|
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/add"
|
||||||
|
# 货易通商品图片添加
|
||||||
|
hytGoodsMediaAdd:
|
||||||
|
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/media/add/batch"
|
||||||
|
# 货易通商品分类添加
|
||||||
|
hytGoodsCategoryAdd:
|
||||||
|
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/good/category/relation/add"
|
||||||
|
# 货易通商品分类查询
|
||||||
|
hytGoodsCategorySearch:
|
||||||
|
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/category/list"
|
||||||
|
# 货易通商品品牌查询
|
||||||
|
hytGoodsBrandSearch:
|
||||||
|
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
default_prompt:
|
default_prompt:
|
||||||
img_recognize:
|
img_recognize:
|
||||||
|
|
|
||||||
|
|
@ -163,6 +163,16 @@ type EinoToolsConfig struct {
|
||||||
HytSupplierSearch ToolConfig `mapstructure:"hytSupplierSearch"`
|
HytSupplierSearch ToolConfig `mapstructure:"hytSupplierSearch"`
|
||||||
// 货易通仓库查询
|
// 货易通仓库查询
|
||||||
HytWarehouseSearch ToolConfig `mapstructure:"hytWarehouseSearch"`
|
HytWarehouseSearch ToolConfig `mapstructure:"hytWarehouseSearch"`
|
||||||
|
// 货易通商品添加
|
||||||
|
HytGoodsAdd ToolConfig `mapstructure:"hytGoodsAdd"`
|
||||||
|
// 货易通商品图片添加
|
||||||
|
HytGoodsMediaAdd ToolConfig `mapstructure:"hytGoodsMediaAdd"`
|
||||||
|
// 货易通商品分类添加
|
||||||
|
HytGoodsCategoryAdd ToolConfig `mapstructure:"hytGoodsCategoryAdd"`
|
||||||
|
// 货易通商品分类查询
|
||||||
|
HytGoodsCategorySearch ToolConfig `mapstructure:"hytGoodsCategorySearch"`
|
||||||
|
// 货易通商品品牌查询
|
||||||
|
HytGoodsBrandSearch ToolConfig `mapstructure:"hytGoodsBrandSearch"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoggingConfig 日志配置
|
// LoggingConfig 日志配置
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ const (
|
||||||
|
|
||||||
// 商品属性模板-中文
|
// 商品属性模板-中文
|
||||||
const (
|
const (
|
||||||
// 货易通商品属性模板-中文
|
// 货易通供应商商品属性模板-中文
|
||||||
HYTProductPropertyTemplateZH = `{
|
HYTSupplierProductPropertyTemplateZH = `{
|
||||||
"货品编号": "string", // 商品编号
|
"货品编号": "string", // 商品编号
|
||||||
"条码": "string", // 货品编号
|
"条码": "string", // 货品编号
|
||||||
"分类名称": "string", // 商品分类
|
"分类名称": "string", // 商品分类
|
||||||
|
|
@ -47,6 +47,33 @@ const (
|
||||||
"默认供应商": "string", // 空
|
"默认供应商": "string", // 空
|
||||||
"默认存放仓库": "string", // 空
|
"默认存放仓库": "string", // 空
|
||||||
}`
|
}`
|
||||||
|
// 货易通商品属性模板-中文 Ps:手机端主图、详情图文、平台资质图 (暂时无需)
|
||||||
|
HYTGoodsAddPropertyTemplateZH = `{
|
||||||
|
"商品标题": "string", // 商品名称
|
||||||
|
"商品编码": "string", // 商品编码
|
||||||
|
"SPU名称": "string", // 商品SPU名称
|
||||||
|
"SPU编码": "string", // 商品编码
|
||||||
|
"商品货号": "string", // 商品货号
|
||||||
|
"商品条形码": "string", // 商品编码
|
||||||
|
"市场价": "string", // 商品市场价 decimal(10,2)
|
||||||
|
"建议销售价": "string", // 商品建议销售价 decimal(10,2)
|
||||||
|
"电商销售价格": "string", // 商品电商销售价格 decimal(10,2)
|
||||||
|
"单位": "string", // 商品单位,若无则使用'个'
|
||||||
|
"折扣(%)": "string", // 商品折扣(%),默认0%
|
||||||
|
"税率(%)": "string", // 商品税率(%),默认13%
|
||||||
|
"运费模版": "string", // 商品运费模版,默认空
|
||||||
|
"保质期": "string", // 商品保质期,无则空
|
||||||
|
"保质期单位": "string", // 商品保质期单位,无则空
|
||||||
|
"品牌": "string", // 商品品牌,若无则空
|
||||||
|
"是否热销主推": "string", // 填否
|
||||||
|
"外部平台链接": "string", // 商品外部平台链接
|
||||||
|
"商品卖点": "string", // 商品卖点
|
||||||
|
"商品规格参数": "string", // 商品规格参数
|
||||||
|
"商品说明": "string", // 商品说明
|
||||||
|
"备注": "string", // 无则空
|
||||||
|
"分类名称": "string", // 商品分类
|
||||||
|
"电脑端主图": ["string"], // 商品电脑端主图
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
// 缓存key
|
// 缓存key
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package goods_add
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.ToolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ToolConfig) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Call(ctx context.Context, req *GoodsAddRequest) (int, error) {
|
||||||
|
apiReq, _ := util.StructToMap(req)
|
||||||
|
|
||||||
|
r := l_request.Request{
|
||||||
|
Method: "Post",
|
||||||
|
Url: c.cfg.BaseURL,
|
||||||
|
Json: apiReq,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := r.Send()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("请求失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resData GoodsAddResponse
|
||||||
|
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||||
|
return 0, fmt.Errorf("解析响应失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resData.Code != 200 {
|
||||||
|
return 0, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resData.Data.Id, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package goods_add
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_Call
|
||||||
|
func Test_Call(t *testing.T) {
|
||||||
|
req := &GoodsAddRequest{
|
||||||
|
Unit: "元",
|
||||||
|
IsComposeGoods: 2,
|
||||||
|
GoodsAttributes: "<p><span style=\"color: rgb(96, 98, 102); background-color: rgb(255, 255, 255); font-size: 14px;\">商品规格参数</span></p>",
|
||||||
|
Introduction: "<p><span style=\"color: rgb(96, 98, 102); background-color: rgb(255, 255, 255); font-size: 14px;\">商品卖点</span></p>",
|
||||||
|
GoodsIllustration: "<p><span style=\"color: rgb(96, 98, 102); background-color: rgb(255, 255, 255); font-size: 14px;\">商品说明</span></p>",
|
||||||
|
IsHot: 2,
|
||||||
|
Title: "fu测试001",
|
||||||
|
GoodsNum: "futest001sku",
|
||||||
|
SpuCode: "futest001spu",
|
||||||
|
SpuName: "fu测试001",
|
||||||
|
Price: 100,
|
||||||
|
SalesPrice: 80,
|
||||||
|
Discount: 15,
|
||||||
|
TaxRate: 13,
|
||||||
|
FreightId: 3,
|
||||||
|
Remark: "备注说明",
|
||||||
|
SellByDate: 180,
|
||||||
|
ExternalPrice: 120,
|
||||||
|
GoodsBarCode: "futest001code2",
|
||||||
|
GoodsCode: "futest001code1",
|
||||||
|
SellByDateUnit: "天",
|
||||||
|
BrandId: 3,
|
||||||
|
ExternalUrl: "https://www.baidu.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.ToolConfig{
|
||||||
|
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/add",
|
||||||
|
}
|
||||||
|
|
||||||
|
client := New(cfg)
|
||||||
|
toolResp, err := client.Call(context.Background(), req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Call() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("toolResp: %v\n", toolResp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package goods_add
|
||||||
|
|
||||||
|
type GoodsAddRequest struct {
|
||||||
|
Title string `json:"title"` // 商品标题
|
||||||
|
GoodsCode string `json:"goods_code"` // 商品编码
|
||||||
|
SpuName string `json:"spu_name"` // SPU 名称
|
||||||
|
SpuCode string `json:"spu_code"` // SPU 编码
|
||||||
|
GoodsNum string `json:"goods_num"` // 商品货号
|
||||||
|
GoodsBarCode string `json:"goods_bar_code"` // 商品条形码
|
||||||
|
Price float64 `json:"price"` // 市场价
|
||||||
|
SalesPrice float64 `json:"sales_price"` // 建议销售价
|
||||||
|
ExternalPrice float64 `json:"external_price"` // 电商销售价格
|
||||||
|
Unit string `json:"unit"` // 价格单位
|
||||||
|
Discount int `json:"discount"` // 折扣
|
||||||
|
TaxRate int `json:"tax_rate"` // 税率
|
||||||
|
FreightId int `json:"freight_id"` // 运费模板 ID
|
||||||
|
SellByDate int `json:"sell_by_date"` // 保质期
|
||||||
|
SellByDateUnit string `json:"sell_by_date_unit"` // 保质期单位
|
||||||
|
BrandId int `json:"brand_id"` // 品牌 ID
|
||||||
|
IsHot int `json:"is_hot"` // 是否热销主推 1.是 2.否(默认)
|
||||||
|
ExternalUrl string `json:"external_url"` // 外部平台链接
|
||||||
|
Introduction string `json:"introduction"` // 商品卖点
|
||||||
|
GoodsAttributes string `json:"goods_attributes"` // 商品规格参数
|
||||||
|
GoodsIllustration string `json:"goods_illustration"` // 商品说明
|
||||||
|
Remark string `json:"remark"` // 备注说明
|
||||||
|
IsComposeGoods int `json:"is_compose_goods"` // 是否组合商品 1.是 2.否(默认)
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsAddResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
Id int `json:"id"` // 商品 ID
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package goods_brand_search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.ToolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ToolConfig) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Call(ctx context.Context, name string) (int, error) {
|
||||||
|
if name == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody := GoodsBrandSearchRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 1,
|
||||||
|
Search: SearchCondition{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiReq, _ := util.StructToMap(reqBody)
|
||||||
|
|
||||||
|
req := l_request.Request{
|
||||||
|
Method: "Post",
|
||||||
|
Url: c.cfg.BaseURL,
|
||||||
|
Json: apiReq,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := req.Send()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("请求失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resData GoodsBrandSearchResponse
|
||||||
|
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||||
|
return 0, fmt.Errorf("解析响应失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resData.Code != 200 {
|
||||||
|
return 0, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resData.Data.List) == 0 {
|
||||||
|
return 0, fmt.Errorf("品牌不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回第一个匹配的品牌ID
|
||||||
|
return resData.Data.List[0].ID, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package goods_brand_search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_Call
|
||||||
|
func Test_Call(t *testing.T) {
|
||||||
|
// 使用示例中的查询条件
|
||||||
|
name := "vivo"
|
||||||
|
|
||||||
|
cfg := config.ToolConfig{
|
||||||
|
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list",
|
||||||
|
}
|
||||||
|
|
||||||
|
client := New(cfg)
|
||||||
|
toolResp, err := client.Call(context.Background(), name)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Call() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("toolResp (BrandID): %v\n", toolResp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package goods_brand_search
|
||||||
|
|
||||||
|
type GoodsBrandSearchRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Search SearchCondition `json:"search"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchCondition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsBrandSearchResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
List []BrandInfo `json:"list"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BrandInfo struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Logo string `json:"logo"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package goods_category_add
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.ToolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ToolConfig) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Call(ctx context.Context, req *GoodsCategoryAddRequest) (bool, error) {
|
||||||
|
apiReq, _ := util.StructToMap(req)
|
||||||
|
|
||||||
|
r := l_request.Request{
|
||||||
|
Method: "Post",
|
||||||
|
Url: c.cfg.BaseURL,
|
||||||
|
Json: apiReq,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := r.Send()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("请求失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resData GoodsCategoryAddResponse
|
||||||
|
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||||
|
return false, fmt.Errorf("解析响应失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resData.Code != 200 {
|
||||||
|
return false, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resData.Data.IsSuccess, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package goods_category_add
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_Call
|
||||||
|
func Test_Call(t *testing.T) {
|
||||||
|
req := &GoodsCategoryAddRequest{
|
||||||
|
GoodsId: 8496,
|
||||||
|
CategoryIds: []int{1667},
|
||||||
|
IsCover: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.ToolConfig{
|
||||||
|
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/good/category/relation/add",
|
||||||
|
}
|
||||||
|
|
||||||
|
client := New(cfg)
|
||||||
|
toolResp, err := client.Call(context.Background(), req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Call() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("toolResp: %v\n", toolResp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package goods_category_add
|
||||||
|
|
||||||
|
type GoodsCategoryAddRequest struct {
|
||||||
|
GoodsId int `json:"goods_id"`
|
||||||
|
CategoryIds []int `json:"category_ids"`
|
||||||
|
IsCover bool `json:"is_cover"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsCategoryAddResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
IsSuccess bool `json:"is_success"` // 是否成功
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package goods_category_search
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.ToolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ToolConfig) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Call(ctx context.Context, name string) (int, error) {
|
||||||
|
if name == "" {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reqBody := GoodsCategorySearchRequest{
|
||||||
|
Page: 1,
|
||||||
|
Limit: 1,
|
||||||
|
Search: SearchCondition{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apiReq, _ := util.StructToMap(reqBody)
|
||||||
|
|
||||||
|
req := l_request.Request{
|
||||||
|
Method: "Post",
|
||||||
|
Url: c.cfg.BaseURL,
|
||||||
|
Json: apiReq,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := req.Send()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("请求失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resData GoodsCategorySearchResponse
|
||||||
|
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||||
|
return 0, fmt.Errorf("解析响应失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resData.Code != 200 {
|
||||||
|
return 0, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resData.Data.List) == 0 {
|
||||||
|
return 0, fmt.Errorf("商品分类不存在")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回第一个匹配的分类ID
|
||||||
|
return resData.Data.List[0].ID, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package goods_category_search
|
||||||
|
|
||||||
|
type GoodsCategorySearchRequest struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
Limit int `json:"limit"`
|
||||||
|
Search SearchCondition `json:"search"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SearchCondition struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsCategorySearchResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
List []CategoryInfo `json:"list"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryInfo struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package goods_media_add
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/pkg/l_request"
|
||||||
|
"ai_scheduler/internal/pkg/util"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg config.ToolConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.ToolConfig) *Client {
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Call(ctx context.Context, req *GoodsMediaAddRequest) (bool, error) {
|
||||||
|
apiReq, _ := util.StructToMap(req)
|
||||||
|
|
||||||
|
r := l_request.Request{
|
||||||
|
Method: "Post",
|
||||||
|
Url: c.cfg.BaseURL,
|
||||||
|
Json: apiReq,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := r.Send()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("请求失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resData GoodsMediaAddResponse
|
||||||
|
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||||
|
return false, fmt.Errorf("解析响应失败,err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resData.Code != 200 {
|
||||||
|
return false, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resData.Data.IsSuccess, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package goods_media_add
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Test_Call
|
||||||
|
func Test_Call(t *testing.T) {
|
||||||
|
req := &GoodsMediaAddRequest{
|
||||||
|
GoodsId: 8496,
|
||||||
|
Data: []MediaItem{
|
||||||
|
{
|
||||||
|
Type: 1,
|
||||||
|
Url: "https://lsxd-hz-store.oss-cn-hangzhou.aliyuncs.com/physicalGoodsSystems/images/goodsimages/goods/22f03d91-3cb7-45b4-ab92-07aad78a1633-screenshot_2025-12-17_17-46-00.png",
|
||||||
|
Sort: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
IsCover: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.ToolConfig{
|
||||||
|
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/media/add/batch",
|
||||||
|
}
|
||||||
|
|
||||||
|
client := New(cfg)
|
||||||
|
toolResp, err := client.Call(context.Background(), req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Call() error = %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("toolResp: %v\n", toolResp)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package goods_media_add
|
||||||
|
|
||||||
|
type GoodsMediaAddRequest struct {
|
||||||
|
GoodsId int `json:"goods_id"`
|
||||||
|
Data []MediaItem `json:"data"`
|
||||||
|
IsCover bool `json:"is_cover"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaItem struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
Url string `json:"url"`
|
||||||
|
Sort int `json:"sort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsMediaAddResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
IsSuccess bool `json:"is_success"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
@ -43,7 +43,7 @@ func (c *Client) Call(ctx context.Context, toolReq *ProductUploadRequest) (toolR
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Msg string `json:"msg"`
|
Msg string `json:"msg"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Ids []int `json:"ids"` // 预览URL
|
Ids []int `json:"ids"` // 商品 IDs
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
var resMap resType
|
var resMap resType
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
package product_upload
|
|
||||||
|
|
||||||
import (
|
|
||||||
"ai_scheduler/internal/config"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Test_Call
|
|
||||||
func Test_Call(t *testing.T) {
|
|
||||||
req := &ProductUploadRequest{
|
|
||||||
SupplierId: 261,
|
|
||||||
WarehouseId: 257,
|
|
||||||
IsDefaultWarehouse: 1,
|
|
||||||
Sort: 1,
|
|
||||||
Profit: 40,
|
|
||||||
TaxRate: 13,
|
|
||||||
GoodsList: []Goods{
|
|
||||||
{
|
|
||||||
GoodsInfo: GoodsInfo{
|
|
||||||
Title: "Apple iPhone 17 Pro Max 星宇橙色 256GB",
|
|
||||||
Brand: "Apple/苹果",
|
|
||||||
Category: "手机",
|
|
||||||
// CostPrice: 9999.00,
|
|
||||||
GoodsAttributes: "CPU型号:A19 Pro;操作系统:iOS;机身存储:256GB;屏幕尺寸:6.86英寸;屏幕材质:OLED直屏;屏幕技术:视网膜XDR;后置摄像头:4800万像素三主摄系统(主摄4800万+超广角4800万+长焦4800万);前置摄像头:1800万像素;网络支持:5G双卡双待(移动/联通/电信);生物识别:人脸识别;防水等级:IP68;充电功率:40W;无线充电:支持;机身尺寸:163.4mm×78.0mm×8.75mm;机身重量:231g;机身颜色:星宇橙色;特征特质:轻薄、防水防尘、无线充电、NFC、磁吸无线充",
|
|
||||||
GoodsBarCode: "10181383848993",
|
|
||||||
GoodsIllustration: "Apple/苹果 iPhone 17 Pro Max 【需当面激活】支持移动联通电信 5G 双卡双待手机 星宇橙色 256GB 官方标配。搭载A19 Pro芯片,6.86英寸OLED视网膜XDR直屏,4800万像素三主摄系统,支持5G双卡双待,IP68防水防尘,40W有线充电,支持无线充电和磁吸充电。",
|
|
||||||
GoodsNum: "10181383848993",
|
|
||||||
Introduction: "Apple/苹果 iPhone 17 Pro Max 【需当面激活】支持移动联通电信 5G 双卡双待手机 星宇橙色 256GB 官方标配。搭载A19 Pro芯片,6.86英寸OLED视网膜XDR直屏,4800万像素三主摄系统,支持5G双卡双待,IP68防水防尘,40W有线充电,支持无线充电和磁吸充电。",
|
|
||||||
IsBind: 1,
|
|
||||||
SpuName: "Apple iPhone 17 Pro Max",
|
|
||||||
SpuNum: "jd_1766038130329_8721",
|
|
||||||
TaxRate: 13,
|
|
||||||
Unit: "台",
|
|
||||||
Weight: "0.231", // 单位:kg
|
|
||||||
Price: 9999.00,
|
|
||||||
SalesPrice: 9999.00,
|
|
||||||
Stock: 0, // JSON 中未提供库存信息
|
|
||||||
Discount: 10, // JSON 中未提供折扣信息
|
|
||||||
IsComposeGoods: 2,
|
|
||||||
IsHot: 2,
|
|
||||||
},
|
|
||||||
GoodsMediaList: []GoodsMedia{
|
|
||||||
{
|
|
||||||
Type: 1,
|
|
||||||
Url: "https://img10.360buyimg.com/pcpubliccms/s228x228_jfs/t1/363919/12/2409/45712/691d9970F84b99d32/9f9a5d5d16efeb79.jpg.avif",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client := New(config.ToolConfig{})
|
|
||||||
toolResp, err := client.Call(context.Background(), req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Call() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("toolResp: %v\n", toolResp)
|
|
||||||
}
|
|
||||||
|
|
@ -41,7 +41,6 @@ func (c *Client) Call(ctx context.Context, name string) (int, error) {
|
||||||
Url: c.cfg.BaseURL,
|
Url: c.cfg.BaseURL,
|
||||||
Json: apiReq,
|
Json: apiReq,
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ func (c *Client) Call(ctx context.Context, name string) (int, error) {
|
||||||
Url: c.cfg.BaseURL,
|
Url: c.cfg.BaseURL,
|
||||||
Params: params,
|
Params: params,
|
||||||
Headers: map[string]string{
|
Headers: map[string]string{
|
||||||
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ package tools
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"ai_scheduler/internal/config"
|
"ai_scheduler/internal/config"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_add"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_brand_search"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_category_search"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_media_add"
|
||||||
"ai_scheduler/internal/domain/tools/hyt/product_upload"
|
"ai_scheduler/internal/domain/tools/hyt/product_upload"
|
||||||
"ai_scheduler/internal/domain/tools/hyt/supplier_search"
|
"ai_scheduler/internal/domain/tools/hyt/supplier_search"
|
||||||
"ai_scheduler/internal/domain/tools/hyt/warehouse_search"
|
"ai_scheduler/internal/domain/tools/hyt/warehouse_search"
|
||||||
|
|
@ -16,6 +21,11 @@ type HytTools struct {
|
||||||
ProductUpload *product_upload.Client
|
ProductUpload *product_upload.Client
|
||||||
SupplierSearch *supplier_search.Client
|
SupplierSearch *supplier_search.Client
|
||||||
WarehouseSearch *warehouse_search.Client
|
WarehouseSearch *warehouse_search.Client
|
||||||
|
GoodsAdd *goods_add.Client
|
||||||
|
GoodsMediaAdd *goods_media_add.Client
|
||||||
|
GoodsCategoryAdd *goods_category_add.Client
|
||||||
|
GoodsCategorySearch *goods_category_search.Client
|
||||||
|
GoodsBrandSearch *goods_brand_search.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager(cfg *config.Config) *Manager {
|
func NewManager(cfg *config.Config) *Manager {
|
||||||
|
|
@ -24,6 +34,11 @@ func NewManager(cfg *config.Config) *Manager {
|
||||||
ProductUpload: product_upload.New(cfg.EinoTools.HytProductUpload),
|
ProductUpload: product_upload.New(cfg.EinoTools.HytProductUpload),
|
||||||
SupplierSearch: supplier_search.New(cfg.EinoTools.HytSupplierSearch),
|
SupplierSearch: supplier_search.New(cfg.EinoTools.HytSupplierSearch),
|
||||||
WarehouseSearch: warehouse_search.New(cfg.EinoTools.HytWarehouseSearch),
|
WarehouseSearch: warehouse_search.New(cfg.EinoTools.HytWarehouseSearch),
|
||||||
|
GoodsAdd: goods_add.New(cfg.EinoTools.HytGoodsAdd),
|
||||||
|
GoodsMediaAdd: goods_media_add.New(cfg.EinoTools.HytGoodsMediaAdd),
|
||||||
|
GoodsCategoryAdd: goods_category_add.New(cfg.EinoTools.HytGoodsCategoryAdd),
|
||||||
|
GoodsCategorySearch: goods_category_search.New(cfg.EinoTools.HytGoodsCategorySearch),
|
||||||
|
GoodsBrandSearch: goods_brand_search.New(cfg.EinoTools.HytGoodsBrandSearch),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,387 @@
|
||||||
|
package hyt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"ai_scheduler/internal/config"
|
||||||
|
errorcode "ai_scheduler/internal/data/error"
|
||||||
|
toolManager "ai_scheduler/internal/domain/tools"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_add"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
|
||||||
|
"ai_scheduler/internal/domain/tools/hyt/goods_media_add"
|
||||||
|
"ai_scheduler/internal/domain/workflow/runtime"
|
||||||
|
"ai_scheduler/internal/entitys"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/compose"
|
||||||
|
)
|
||||||
|
|
||||||
|
const WorkflowIDGoodsAdd = "hyt.goodsAdd"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
runtime.Register(WorkflowIDGoodsAdd, func(d *runtime.Deps) (runtime.Workflow, error) {
|
||||||
|
return &goodsAdd{cfg: d.Conf, toolManager: d.ToolManager}, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type goodsAdd struct {
|
||||||
|
cfg *config.Config
|
||||||
|
toolManager *toolManager.Manager
|
||||||
|
data *GoodsAddWorkflowInput
|
||||||
|
}
|
||||||
|
|
||||||
|
type GoodsAddWorkflowInput struct {
|
||||||
|
Text string `mapstructure:"text"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *goodsAdd) ID() string { return WorkflowIDGoodsAdd }
|
||||||
|
|
||||||
|
func (o *goodsAdd) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) {
|
||||||
|
// 构建工作流
|
||||||
|
runnable, err := o.buildWorkflow(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.data = &GoodsAddWorkflowInput{
|
||||||
|
Text: rec.UserContent.Text,
|
||||||
|
}
|
||||||
|
// 工作流过程调用
|
||||||
|
output, err := runnable.Invoke(ctx, o.data)
|
||||||
|
if err != nil {
|
||||||
|
errStr := err.Error()
|
||||||
|
if u := errors.Unwrap(err); u != nil {
|
||||||
|
errStr = u.Error()
|
||||||
|
}
|
||||||
|
return nil, errorcode.WorkflowErr(errStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductIngestData 对应 HYTGoodsAddPropertyTemplateZH 的结构
|
||||||
|
type GoodsAddProductIngestData struct {
|
||||||
|
Title string `json:"商品标题"`
|
||||||
|
GoodsCode string `json:"商品编码"`
|
||||||
|
SpuName string `json:"SPU名称"`
|
||||||
|
SpuCode string `json:"SPU编码"`
|
||||||
|
GoodsNum string `json:"商品货号"`
|
||||||
|
GoodsBarCode string `json:"商品条形码"`
|
||||||
|
Price string `json:"市场价"`
|
||||||
|
SalesPrice string `json:"建议销售价"`
|
||||||
|
ExternalPrice string `json:"电商销售价格"`
|
||||||
|
Unit string `json:"单位"`
|
||||||
|
Discount string `json:"折扣(%)"`
|
||||||
|
TaxRate string `json:"税率(%)"`
|
||||||
|
FreightTemplate string `json:"运费模版"`
|
||||||
|
SellByDate string `json:"保质期"`
|
||||||
|
SellByDateUnit string `json:"保质期单位"`
|
||||||
|
Brand string `json:"品牌"`
|
||||||
|
IsHot string `json:"是否热销主推"`
|
||||||
|
ExternalUrl string `json:"外部平台链接"`
|
||||||
|
Introduction string `json:"商品卖点"`
|
||||||
|
GoodsAttributes string `json:"商品规格参数"`
|
||||||
|
GoodsIllustration string `json:"商品说明"`
|
||||||
|
Remark string `json:"备注"`
|
||||||
|
CategoryName string `json:"分类名称"`
|
||||||
|
Images []string `json:"电脑端主图"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoodsAddContext Graph 执行上下文状态
|
||||||
|
type GoodsAddContext struct {
|
||||||
|
mu *sync.Mutex
|
||||||
|
InputText string
|
||||||
|
IngestData *GoodsAddProductIngestData
|
||||||
|
|
||||||
|
// 核心请求体
|
||||||
|
AddGoodsReq *goods_add.GoodsAddRequest
|
||||||
|
|
||||||
|
// 中间态数据
|
||||||
|
BrandId int
|
||||||
|
CategoryId int
|
||||||
|
BrandName string
|
||||||
|
CategoryName string
|
||||||
|
|
||||||
|
// 运行结果
|
||||||
|
GoodsId int
|
||||||
|
Result map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWorkflow 构建基于 Graph 的并行工作流
|
||||||
|
func (o *goodsAdd) buildWorkflow(ctx context.Context) (compose.Runnable[*GoodsAddWorkflowInput, map[string]any], error) {
|
||||||
|
g := compose.NewGraph[*GoodsAddWorkflowInput, map[string]any]()
|
||||||
|
|
||||||
|
// 1. DataMapping 节点: 解析 JSON -> 填充基础 Request
|
||||||
|
g.AddLambdaNode("data_mapping", compose.InvokableLambda(func(ctx context.Context, in *GoodsAddWorkflowInput) (*GoodsAddContext, error) {
|
||||||
|
state := &GoodsAddContext{
|
||||||
|
mu: &sync.Mutex{}, // 初始化锁
|
||||||
|
InputText: in.Text,
|
||||||
|
AddGoodsReq: &goods_add.GoodsAddRequest{},
|
||||||
|
Result: make(map[string]any),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析用户输入的中文 JSON
|
||||||
|
var ingestData GoodsAddProductIngestData
|
||||||
|
if err := json.Unmarshal([]byte(in.Text), &ingestData); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析商品数据失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 必填校验
|
||||||
|
if ingestData.Title == "" {
|
||||||
|
return nil, errors.New("商品标题不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.GoodsCode == "" {
|
||||||
|
return nil, errors.New("商品编码不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.SpuName == "" {
|
||||||
|
return nil, errors.New("SPU名称不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.SpuCode == "" {
|
||||||
|
return nil, errors.New("SPU编码不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.Price == "" {
|
||||||
|
return nil, errors.New("市场价不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.SalesPrice == "" {
|
||||||
|
return nil, errors.New("建议销售价不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.Unit == "" {
|
||||||
|
return nil, errors.New("价格单位不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.Discount == "" {
|
||||||
|
return nil, errors.New("折扣不能为空")
|
||||||
|
}
|
||||||
|
if ingestData.TaxRate == "" {
|
||||||
|
return nil, errors.New("税率不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.IngestData = &ingestData
|
||||||
|
state.BrandName = ingestData.Brand
|
||||||
|
state.CategoryName = ingestData.CategoryName
|
||||||
|
|
||||||
|
// 映射字段到 AddGoodsReq
|
||||||
|
state.AddGoodsReq.Title = ingestData.Title
|
||||||
|
state.AddGoodsReq.GoodsCode = ingestData.GoodsCode
|
||||||
|
state.AddGoodsReq.SpuName = ingestData.SpuName
|
||||||
|
state.AddGoodsReq.SpuCode = ingestData.SpuCode
|
||||||
|
state.AddGoodsReq.GoodsNum = ingestData.GoodsNum
|
||||||
|
state.AddGoodsReq.GoodsBarCode = ingestData.GoodsBarCode
|
||||||
|
|
||||||
|
// 价格处理
|
||||||
|
if val, err := strconv.ParseFloat(strings.TrimSuffix(ingestData.Price, "元"), 64); err == nil {
|
||||||
|
state.AddGoodsReq.Price = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.ParseFloat(strings.TrimSuffix(ingestData.SalesPrice, "元"), 64); err == nil {
|
||||||
|
state.AddGoodsReq.SalesPrice = val
|
||||||
|
}
|
||||||
|
if val, err := strconv.ParseFloat(strings.TrimSuffix(ingestData.ExternalPrice, "元"), 64); err == nil && state.AddGoodsReq.Price == 0 {
|
||||||
|
state.AddGoodsReq.ExternalPrice = val
|
||||||
|
}
|
||||||
|
|
||||||
|
state.AddGoodsReq.Unit = ingestData.Unit
|
||||||
|
|
||||||
|
// 折扣处理 "80%" -> 80
|
||||||
|
discountStr := strings.TrimSuffix(ingestData.Discount, "%")
|
||||||
|
if val, err := strconv.Atoi(discountStr); err == nil {
|
||||||
|
state.AddGoodsReq.Discount = val
|
||||||
|
}
|
||||||
|
// 税率处理 "13%" -> 13
|
||||||
|
taxStr := strings.TrimSuffix(strings.TrimSuffix(ingestData.TaxRate, "%"), " ")
|
||||||
|
if val, err := strconv.Atoi(taxStr); err == nil {
|
||||||
|
state.AddGoodsReq.TaxRate = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运费模板先不给 state.AddGoodsReq.FreightId = 3
|
||||||
|
|
||||||
|
// 保质期处理 "180天" -> 180
|
||||||
|
sellByDateStr := strings.TrimSuffix(ingestData.SellByDate, "天")
|
||||||
|
if val, err := strconv.Atoi(sellByDateStr); err == nil {
|
||||||
|
state.AddGoodsReq.SellByDate = val
|
||||||
|
}
|
||||||
|
state.AddGoodsReq.SellByDateUnit = ingestData.SellByDateUnit
|
||||||
|
|
||||||
|
// state.AddGoodsReq.BrandId 品牌ID后续赋值
|
||||||
|
|
||||||
|
state.AddGoodsReq.IsHot = 2
|
||||||
|
if ingestData.IsHot == "是" {
|
||||||
|
state.AddGoodsReq.IsHot = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
state.AddGoodsReq.ExternalUrl = ingestData.ExternalUrl
|
||||||
|
state.AddGoodsReq.Introduction = ingestData.Introduction
|
||||||
|
state.AddGoodsReq.GoodsAttributes = ingestData.GoodsAttributes
|
||||||
|
state.AddGoodsReq.GoodsIllustration = ingestData.GoodsIllustration
|
||||||
|
state.AddGoodsReq.Remark = ingestData.Remark
|
||||||
|
state.AddGoodsReq.IsComposeGoods = 2 // 非组合商品
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 2. 获取品牌ID 节点 (并行)
|
||||||
|
g.AddLambdaNode("get_brand_id", compose.InvokableLambda(func(ctx context.Context, state *GoodsAddContext) (*GoodsAddContext, error) {
|
||||||
|
if state.BrandName == "" {
|
||||||
|
return state, errors.New("品牌名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
brandId, err := o.toolManager.Hyt.GoodsBrandSearch.Call(ctx, state.BrandName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: 品牌ID获取失败,%s: %v\n", state.BrandName, err)
|
||||||
|
// 如果获取失败,不阻断后续流程
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state.mu.Lock()
|
||||||
|
defer state.mu.Unlock()
|
||||||
|
state.BrandId = brandId
|
||||||
|
state.AddGoodsReq.BrandId = brandId
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 3. 获取分类ID 节点 (并行)
|
||||||
|
g.AddLambdaNode("get_category_id", compose.InvokableLambda(func(ctx context.Context, state *GoodsAddContext) (*GoodsAddContext, error) {
|
||||||
|
if state.CategoryName == "" {
|
||||||
|
return state, errors.New("分类名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
categoryId, err := o.toolManager.Hyt.GoodsCategorySearch.Call(ctx, state.CategoryName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: 分类ID获取失败,%s: %v\n", state.CategoryName, err)
|
||||||
|
// 如果获取失败,不阻断后续流程
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state.mu.Lock()
|
||||||
|
defer state.mu.Unlock()
|
||||||
|
state.CategoryId = categoryId
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 4. 新增商品 节点 (依赖 get_brand_id)
|
||||||
|
g.AddLambdaNode("goods_add", compose.InvokableLambda(func(ctx context.Context, state *GoodsAddContext) (*GoodsAddContext, error) {
|
||||||
|
// 校验 BrandId
|
||||||
|
if state.AddGoodsReq.BrandId == 0 {
|
||||||
|
return nil, errors.New("Missing Brand ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 goods_add 工具
|
||||||
|
goodsId, err := o.toolManager.Hyt.GoodsAdd.Call(ctx, state.AddGoodsReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("新增商品失败: %w", err)
|
||||||
|
}
|
||||||
|
state.GoodsId = goodsId
|
||||||
|
|
||||||
|
state.Result["goods_id"] = state.GoodsId
|
||||||
|
state.Result["spu_code"] = state.AddGoodsReq.SpuCode
|
||||||
|
return state, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 5. 新增商品分类 节点 (依赖 goods_add 和 get_category_id)
|
||||||
|
g.AddLambdaNode("goods_category_add", compose.InvokableLambda(func(ctx context.Context, state *GoodsAddContext) (*GoodsAddContext, error) {
|
||||||
|
if state.GoodsId == 0 {
|
||||||
|
return nil, errors.New("goods_id is 0")
|
||||||
|
}
|
||||||
|
if state.CategoryId == 0 {
|
||||||
|
return nil, errors.New("category_id is 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &goods_category_add.GoodsCategoryAddRequest{
|
||||||
|
GoodsId: state.GoodsId,
|
||||||
|
CategoryIds: []int{state.CategoryId},
|
||||||
|
IsCover: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := o.toolManager.Hyt.GoodsCategoryAdd.Call(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: 关联分类失败: %v", err)
|
||||||
|
state.mu.Lock()
|
||||||
|
state.Result["category_error"] = err.Error()
|
||||||
|
state.mu.Unlock()
|
||||||
|
} else {
|
||||||
|
state.mu.Lock()
|
||||||
|
state.Result["category_added"] = true
|
||||||
|
state.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 6. 新增商品图片 节点 (依赖 goods_add)
|
||||||
|
g.AddLambdaNode("goods_media_add", compose.InvokableLambda(func(ctx context.Context, state *GoodsAddContext) (*GoodsAddContext, error) {
|
||||||
|
if state.GoodsId == 0 {
|
||||||
|
return nil, errors.New("goods_id is 0")
|
||||||
|
}
|
||||||
|
if len(state.IngestData.Images) == 0 {
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &goods_media_add.GoodsMediaAddRequest{
|
||||||
|
GoodsId: state.GoodsId,
|
||||||
|
IsCover: true,
|
||||||
|
Data: make([]goods_media_add.MediaItem, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, url := range state.IngestData.Images {
|
||||||
|
req.Data = append(req.Data, goods_media_add.MediaItem{
|
||||||
|
Type: 1, // 图片
|
||||||
|
Url: url,
|
||||||
|
Sort: i,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := o.toolManager.Hyt.GoodsMediaAdd.Call(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("warning: 添加图片失败: %v", err)
|
||||||
|
state.mu.Lock()
|
||||||
|
state.Result["media_error"] = err.Error()
|
||||||
|
state.mu.Unlock()
|
||||||
|
} else {
|
||||||
|
state.mu.Lock()
|
||||||
|
state.Result["media_added"] = true
|
||||||
|
state.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return state, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 7. 结果格式化节点
|
||||||
|
g.AddLambdaNode("format_output", compose.InvokableLambda(func(ctx context.Context, state *GoodsAddContext) (map[string]any, error) {
|
||||||
|
return state.Result, nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 构建边 (DAG)
|
||||||
|
|
||||||
|
// Start -> DataMapping
|
||||||
|
g.AddEdge(compose.START, "data_mapping")
|
||||||
|
|
||||||
|
// Branching: DataMapping -> GetBrandId, DataMapping -> GetCategoryId
|
||||||
|
g.AddEdge("data_mapping", "get_brand_id")
|
||||||
|
g.AddEdge("data_mapping", "get_category_id")
|
||||||
|
|
||||||
|
// Synchronization for GoodsAdd: Need BrandId
|
||||||
|
g.AddEdge("get_brand_id", "goods_add")
|
||||||
|
|
||||||
|
// Synchronization for CategoryAdd: Need GoodsId AND CategoryId
|
||||||
|
// Eino supports multi-predecessor nodes which act as merge points.
|
||||||
|
// state merging is handled by the framework (usually last writer wins or custom merge, but here we modify different fields/mutex).
|
||||||
|
// However, we need to ensure goods_add is done.
|
||||||
|
g.AddEdge("goods_add", "goods_category_add")
|
||||||
|
g.AddEdge("get_category_id", "goods_category_add")
|
||||||
|
|
||||||
|
// Synchronization for MediaAdd: Need GoodsId
|
||||||
|
g.AddEdge("goods_add", "goods_media_add")
|
||||||
|
|
||||||
|
// Final Merge
|
||||||
|
g.AddEdge("goods_category_add", "format_output")
|
||||||
|
g.AddEdge("goods_media_add", "format_output")
|
||||||
|
|
||||||
|
g.AddEdge("format_output", compose.END)
|
||||||
|
|
||||||
|
return g.Compile(ctx)
|
||||||
|
}
|
||||||
|
|
@ -19,10 +19,10 @@ import (
|
||||||
"github.com/cloudwego/eino/compose"
|
"github.com/cloudwego/eino/compose"
|
||||||
)
|
)
|
||||||
|
|
||||||
const WorkflowID = "hyt.productUpload"
|
const WorkflowIDProductUpload = "hyt.productUpload"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runtime.Register(WorkflowID, func(d *runtime.Deps) (runtime.Workflow, error) {
|
runtime.Register(WorkflowIDProductUpload, func(d *runtime.Deps) (runtime.Workflow, error) {
|
||||||
return &productUpload{cfg: d.Conf, toolManager: d.ToolManager}, nil
|
return &productUpload{cfg: d.Conf, toolManager: d.ToolManager}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ type ProductUploadWorkflowInput struct {
|
||||||
Text string `mapstructure:"text"`
|
Text string `mapstructure:"text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *productUpload) ID() string { return WorkflowID }
|
func (o *productUpload) ID() string { return WorkflowIDProductUpload }
|
||||||
|
|
||||||
func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) {
|
func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) {
|
||||||
// 构建工作流
|
// 构建工作流
|
||||||
|
|
@ -64,8 +64,8 @@ func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProductIngestData 对应 HYTProductPropertyTemplateZH 的结构
|
// ProductIngestData 对应 HYTSupplierProductPropertyTemplateZH 的结构
|
||||||
type ProductIngestData struct {
|
type SupplierProductIngestData struct {
|
||||||
BarCode string `json:"条码"`
|
BarCode string `json:"条码"`
|
||||||
CategoryName string `json:"分类名称"`
|
CategoryName string `json:"分类名称"`
|
||||||
GoodsName string `json:"货品名称"`
|
GoodsName string `json:"货品名称"`
|
||||||
|
|
@ -99,7 +99,7 @@ type ProductIngestData struct {
|
||||||
type ProductUploadContext struct {
|
type ProductUploadContext struct {
|
||||||
mu *sync.Mutex
|
mu *sync.Mutex
|
||||||
InputText string
|
InputText string
|
||||||
IngestData *ProductIngestData
|
IngestData *SupplierProductIngestData
|
||||||
UploadReq *toolPu.ProductUploadRequest
|
UploadReq *toolPu.ProductUploadRequest
|
||||||
SupplierName string
|
SupplierName string
|
||||||
WarehouseName string
|
WarehouseName string
|
||||||
|
|
@ -121,7 +121,7 @@ func (o *productUpload) buildWorkflow(ctx context.Context) (compose.Runnable[*Pr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析用户输入的中文 JSON
|
// 解析用户输入的中文 JSON
|
||||||
var ingestData ProductIngestData
|
var ingestData SupplierProductIngestData
|
||||||
if err := json.Unmarshal([]byte(in.Text), &ingestData); err != nil {
|
if err := json.Unmarshal([]byte(in.Text), &ingestData); err != nil {
|
||||||
return nil, fmt.Errorf("解析商品数据失败: %w", err)
|
return nil, fmt.Errorf("解析商品数据失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
|
||||||
var sysProductPropertyTemplateZH string
|
var sysProductPropertyTemplateZH string
|
||||||
switch req.SysId {
|
switch req.SysId {
|
||||||
case "hyt": // 货易通
|
case "hyt": // 货易通
|
||||||
sysProductPropertyTemplateZH = constants.HYTProductPropertyTemplateZH
|
sysProductPropertyTemplateZH = constants.HYTGoodsAddPropertyTemplateZH
|
||||||
default:
|
default:
|
||||||
return errorcode.ParamErrf("invalid sys_id")
|
return errorcode.ParamErrf("invalid sys_id")
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +191,7 @@ func (s *CapabilityService) ProductIngestConfirm(c *fiber.Ctx) error {
|
||||||
switch respData.SysId {
|
switch respData.SysId {
|
||||||
// 货易通
|
// 货易通
|
||||||
case "hyt":
|
case "hyt":
|
||||||
workflowId = hytWorkflow.WorkflowID
|
workflowId = hytWorkflow.WorkflowIDGoodsAdd
|
||||||
default:
|
default:
|
||||||
return errorcode.ParamErr("invalid sys_id")
|
return errorcode.ParamErr("invalid sys_id")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue