feat: 1.新增多个货易通工具 2.新增货易通创建商品工作流

This commit is contained in:
fuzhongyun 2025-12-24 11:52:00 +08:00
parent 8d4f3c494e
commit 208f749483
25 changed files with 1021 additions and 81 deletions

View File

@ -88,6 +88,23 @@ eino_tools:
# 货易通仓库查询
hytWarehouseSearch:
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:
img_recognize:

View File

@ -163,6 +163,16 @@ type EinoToolsConfig struct {
HytSupplierSearch ToolConfig `mapstructure:"hytSupplierSearch"`
// 货易通仓库查询
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 日志配置

View File

@ -17,8 +17,8 @@ const (
// 商品属性模板-中文
const (
// 货易通商品属性模板-中文
HYTProductPropertyTemplateZH = `{
// 货易通供应商商品属性模板-中文
HYTSupplierProductPropertyTemplateZH = `{
"货品编号": "string", // 商品编号
"条码": "string", // 货品编号
"分类名称": "string", // 商品分类
@ -47,6 +47,33 @@ const (
"默认供应商": "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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -43,7 +43,7 @@ func (c *Client) Call(ctx context.Context, toolReq *ProductUploadRequest) (toolR
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Ids []int `json:"ids"` // 预览URL
Ids []int `json:"ids"` // 商品 IDs
} `json:"data"`
}
var resMap resType

View File

@ -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)
}

View File

@ -41,7 +41,6 @@ func (c *Client) Call(ctx context.Context, name string) (int, error) {
Url: c.cfg.BaseURL,
Json: apiReq,
Headers: map[string]string{
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
"Content-Type": "application/json",
},
}

View File

@ -36,7 +36,6 @@ func (c *Client) Call(ctx context.Context, name string) (int, error) {
Url: c.cfg.BaseURL,
Params: params,
Headers: map[string]string{
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
"Content-Type": "application/json",
},
}

View File

@ -2,6 +2,11 @@ package tools
import (
"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/supplier_search"
"ai_scheduler/internal/domain/tools/hyt/warehouse_search"
@ -13,17 +18,27 @@ type Manager struct {
}
type HytTools struct {
ProductUpload *product_upload.Client
SupplierSearch *supplier_search.Client
WarehouseSearch *warehouse_search.Client
ProductUpload *product_upload.Client
SupplierSearch *supplier_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 {
return &Manager{
Hyt: &HytTools{
ProductUpload: product_upload.New(cfg.EinoTools.HytProductUpload),
SupplierSearch: supplier_search.New(cfg.EinoTools.HytSupplierSearch),
WarehouseSearch: warehouse_search.New(cfg.EinoTools.HytWarehouseSearch),
ProductUpload: product_upload.New(cfg.EinoTools.HytProductUpload),
SupplierSearch: supplier_search.New(cfg.EinoTools.HytSupplierSearch),
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),
},
}
}

View File

@ -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)
}

View File

@ -19,10 +19,10 @@ import (
"github.com/cloudwego/eino/compose"
)
const WorkflowID = "hyt.productUpload"
const WorkflowIDProductUpload = "hyt.productUpload"
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
})
}
@ -37,7 +37,7 @@ type ProductUploadWorkflowInput struct {
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) {
// 构建工作流
@ -64,8 +64,8 @@ func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map
return output, nil
}
// ProductIngestData 对应 HYTProductPropertyTemplateZH 的结构
type ProductIngestData struct {
// ProductIngestData 对应 HYTSupplierProductPropertyTemplateZH 的结构
type SupplierProductIngestData struct {
BarCode string `json:"条码"`
CategoryName string `json:"分类名称"`
GoodsName string `json:"货品名称"`
@ -99,7 +99,7 @@ type ProductIngestData struct {
type ProductUploadContext struct {
mu *sync.Mutex
InputText string
IngestData *ProductIngestData
IngestData *SupplierProductIngestData
UploadReq *toolPu.ProductUploadRequest
SupplierName string
WarehouseName string
@ -121,7 +121,7 @@ func (o *productUpload) buildWorkflow(ctx context.Context) (compose.Runnable[*Pr
}
// 解析用户输入的中文 JSON
var ingestData ProductIngestData
var ingestData SupplierProductIngestData
if err := json.Unmarshal([]byte(in.Text), &ingestData); err != nil {
return nil, fmt.Errorf("解析商品数据失败: %w", err)
}

View File

@ -76,7 +76,7 @@ func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
var sysProductPropertyTemplateZH string
switch req.SysId {
case "hyt": // 货易通
sysProductPropertyTemplateZH = constants.HYTProductPropertyTemplateZH
sysProductPropertyTemplateZH = constants.HYTGoodsAddPropertyTemplateZH
default:
return errorcode.ParamErrf("invalid sys_id")
}
@ -191,7 +191,7 @@ func (s *CapabilityService) ProductIngestConfirm(c *fiber.Ctx) error {
switch respData.SysId {
// 货易通
case "hyt":
workflowId = hytWorkflow.WorkflowID
workflowId = hytWorkflow.WorkflowIDGoodsAdd
default:
return errorcode.ParamErr("invalid sys_id")
}