1. 新增货易通商品上传工具
2. 新增货易通商品上传工作流
3. 新增商品上传至货易通接口
This commit is contained in:
fuzhongyun 2025-12-19 18:38:06 +08:00
parent 8414a57661
commit 5b789e557a
12 changed files with 548 additions and 62 deletions

View File

@ -74,6 +74,9 @@ tools:
zltxOrderAfterSaleResellerBatch:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
hytProductUpload:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/oursProduct"

View File

@ -141,6 +141,8 @@ type ToolsConfig struct {
CozeExpress ToolConfig `mapstructure:"cozeExpress"`
// Coze 公司查询工具
CozeCompany ToolConfig `mapstructure:"cozeCompany"`
// 货易通商品上传
HytProductUpload ToolConfig `mapstructure:"hytProductUpload"`
}
// ToolConfig 单个工具配置

View File

@ -0,0 +1,174 @@
package constants
// Token
const (
CapabilityProductIngestToken = "A7f9KQ3mP2X8LZC4R5e"
)
// Prompt
const (
SystemPrompt = `
#你是一个专业的商品属性提取助手你的任务是根据用户输入提取商品的属性信息
目标属性模板%s
1.最终输出格式为纯JSON字符串键值对对应目标属性和提取到的属性值
2.最终输出不要携带markdown标识不要携带回车换行`
)
// 商品属性模板-中文
const (
// 货易通商品属性模板-中文
HYTProductPropertyTemplateZH = `{
"条码": "string", // 商品编号
"分类名称": "string", // 商品分类
"货品名称": "string", // 商品名称
"货品编号": "string", // 商品编号
"商品货号": "string", // 商品编号
"品牌": "string", // 商品品牌
"单位": "string", // 商品单位,若无则使用'个'
"规格参数": "string", // 商品规格参数
"货品说明": "string", // 商品说明
"保质期": "string", // 商品保质期
"保质期单位": "string", // 商品保质期单位
"链接": "string", //
"货品图片": ["string"], // 商品多图取1-2个即可
"电商销售价格": "decimal(10,2)", // 商品电商销售价格
"销售价": "decimal(10,2)", // 商品销售价格
"供应商报价": "decimal(10,2)", // 商品供应商报价
"税率": "number%", // 商品税率 x%
"默认供应商": "", // 空即可
"默认存放仓库": "", // 空即可
"备注": "", // 备注
"长": "string", // 商品长度decimal(10,2)+单位
"宽": "string", // 商品宽度decimal(10,2)+单位
"高": "string", // 商品高度decimal(10,2)+单位
"重量": "string", // 商品重量(kg)
"SPU名称": "string", // 商品SPU名称
"SPU编码": "string" // 编码串jd_{timestamp}_rand(1000-999)
}`
)
// 商品属性模板
const (
HYTProductPropertyTemplate = `{
"important_data": {
"type": "object",
"properties": {
"supplier_name": {
"type": "string",
"description": "供应商名称"
},
"warehouse_name": {
"type": "string",
"description": "仓库名称"
},
"profit": {
"type": "float64",
"description": "利润 decimal(10,2)"
},
"tax_rate": {
"type": "integer",
"description": "税率 (x)%"
}
}
},
"goods_info": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "商品名称"
},
"brand": {
"type": "string",
"description": "品牌"
},
"category": {
"type": "string",
"description": "分类"
},
"price": {
"type": "float64",
"description": "市场价 decimal(10,2)"
},
"sales_price": {
"type": "float64",
"description": "建议销售价 decimal(10,2)"
},
"discount": {
"type": "integer",
"description": "折扣百分比 公式:(市场价-建议销售价)/市场价*100"
},
"goods_attributes": {
"type": "string",
"description": "商品属性"
},
"goods_bar_code": {
"type": "string",
"description": "商品条码"
},
"goods_illustration": {
"type": "string",
"description": "商品插图"
},
"goods_num": {
"type": "string",
"description": "商品编号"
},
"introduction": {
"type": "string",
"description": "商品介绍"
},
"spu_name": {
"type": "string",
"description": "SPU名称"
},
"spu_num": {
"type": "string",
"description": "SPU编号"
},
"stock": {
"type": "integer",
"description": "库存"
},
"tax_rate": {
"type": "integer",
"description": "税率"
},
"unit": {
"type": "string",
"description": "单位"
},
"weight": {
"type": "string",
"description": "重量"
}
}
},
"goods_media_list": {
"type": "array",
"description": "商品媒体文件列表",
"items": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "媒体文件URL"
},
"type": {
"type": "integer",
"description": "媒体类型(1:图片, 2:视频)"
},
"sort": {
"type": "integer",
"description": "排序序号"
}
}
}
}
}`
)
// 外部平台地址
const (
HYTProductListPageURL = "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
)

View File

@ -0,0 +1,60 @@
package product_upload
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/util"
"context"
"encoding/json"
"errors"
)
func Call(ctx context.Context, cfg config.ToolConfig, toolReq *ProductUploadRequest) (toolResp *ProductUploadResponse, err error) {
// 商品有且只能有一个
if len(toolReq.GoodsList) != 1 {
err = errors.New("商品只能有一个")
return
}
apiReq, _ := util.StructToMap(toolReq)
req := l_request.Request{
Method: "Post",
Url: "http://120.55.12.245:8100/api/v1/goods/supplier/batch/add/complete",
Json: apiReq,
}
res, err := req.Send()
if err != nil {
return
}
type resType struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
Ids []int `json:"ids"` // 预览URL
} `json:"data"`
}
var resMap resType
err = json.Unmarshal([]byte(res.Text), &resMap)
if err != nil {
return
}
if resMap.Code != 200 {
err = errors.New("货易通商品创建失败")
return
}
if len(resMap.Data.Ids) == 0 {
err = errors.New("货易通商品创建失败")
return
}
toolResp = &ProductUploadResponse{
PreviewUrl: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage",
SpuNum: toolReq.GoodsList[0].GoodsInfo.SpuNum,
Id: resMap.Data.Ids[0],
}
return toolResp, nil
}

View File

@ -0,0 +1,60 @@
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",
},
},
},
},
}
toolResp, err := Call(context.Background(), config.ToolConfig{}, req)
if err != nil {
t.Errorf("Call() error = %v", err)
}
fmt.Printf("toolResp: %v\n", toolResp)
}

View File

@ -0,0 +1,54 @@
package product_upload
type ProductUploadRequest struct {
SupplierId int `json:"supplier_id"` // 供应商ID
WarehouseId int `json:"warehouse_id"` // 仓库ID
IsDefaultWarehouse int `json:"is_default_warehouse"` // 是否默认仓库
Sort int `json:"sort"` // 排序
Profit float64 `json:"profit"` // 利润
TaxRate int `json:"tax_rate"` // 税率
GoodsList []Goods `json:"goods_list"` // 商品列表
}
type Goods struct {
GoodsInfo GoodsInfo `json:"goods_info"`
GoodsMediaList []GoodsMedia `json:"goods_media_list"`
}
type GoodsInfo struct {
Title string `json:"title"` // 商品名称
Brand string `json:"brand"` // 品牌
Category string `json:"category"` // 分类
Discount int `json:"discount"` // 折扣
GoodsAttributes string `json:"goods_attributes"` // 商品属性
GoodsBarCode string `json:"goods_bar_code"` // 商品条码
GoodsNum string `json:"goods_num"` // 商品编号
Introduction string `json:"introduction"` // 商品介绍
SpuName string `json:"spu_name"` // SPU名称
SpuNum string `json:"spu_num"` // SPU编号
Stock int `json:"stock"` // 库存
TaxRate int `json:"tax_rate"` // 税率
Unit string `json:"unit"` // 单位
Weight string `json:"weight"` // 重量
Price float64 `json:"price"` // 市场价
SalesPrice float64 `json:"sales_price"` // 建议销售价格
GoodsIllustration string `json:"goods_illustration"` // 商品插图 - 暂不提供
Id int `json:"id"` // 商品ID - 无需
CostPrice float64 `json:"cost_price"` // 成本价格 - 无需
IsBind int `json:"is_bind"` // 是否绑定 - 默认0
IsComposeGoods int32 `json:"is_compose_goods"` // 是否组合商品 - 默认2
IsHot int `json:"is_hot"` // 是否热门商品 - 默认2
}
type GoodsMedia struct {
Remark string `json:"remark"` // 备注
Sort int `json:"sort"` // 排序
Type int `json:"type"` // 类型
Url string `json:"url"` // URL
}
type ProductUploadResponse struct {
PreviewUrl string `json:"preview_url"` // 预览URL
SpuNum string `json:"spu_code"` // SPU编码
Id int `json:"id"` // 商品ID
}

View File

@ -0,0 +1,112 @@
package hyt
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
toolHytPu "ai_scheduler/internal/domain/tools/hyt/product_upload"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"context"
"encoding/json"
"fmt"
eino_ollama "github.com/cloudwego/eino-ext/components/model/ollama"
"github.com/cloudwego/eino/components/prompt"
"github.com/cloudwego/eino/compose"
"github.com/cloudwego/eino/schema"
)
const WorkflowID = "hyt.productUpload"
func init() {
runtime.Register(WorkflowID, func(d *runtime.Deps) (runtime.Workflow, error) {
return &productUpload{cfg: d.Conf}, nil
})
}
type productUpload struct {
cfg *config.Config
data *ProductUploadWorkflowInput
}
type ProductUploadWorkflowInput struct {
Text string `mapstructure:"text"`
}
func (o *productUpload) ID() string { return WorkflowID }
func (o *productUpload) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) {
// 构建工作流
chain, err := o.buildWorkflow(ctx)
if err != nil {
return nil, err
}
o.data = &ProductUploadWorkflowInput{
Text: rec.UserContent.Text,
}
// 工作流过程调用
output, err := chain.Invoke(ctx, o.data)
if err != nil {
return nil, err
}
fmt.Printf("workflow output: %v\n", output)
// 不关心输出,全部在途中输出
return output, nil
}
func (o *productUpload) buildWorkflow(ctx context.Context) (compose.Runnable[*ProductUploadWorkflowInput, map[string]any], error) {
// 定义工作流
c := compose.NewChain[*ProductUploadWorkflowInput, map[string]any]()
// AI映射工具所需参数'
paramMappingModel, err := eino_ollama.NewChatModel(ctx, &eino_ollama.ChatModelConfig{
BaseURL: o.cfg.Ollama.BaseURL,
Timeout: o.cfg.Ollama.Timeout,
Model: o.cfg.Ollama.Model,
Thinking: &eino_ollama.ThinkValue{Value: true},
Options: &eino_ollama.Options{Temperature: 0.5},
})
if err != nil {
return nil, err
}
// 1. 构建参LLM数映射提示词
c.AppendChatTemplate(prompt.FromMessages(
schema.FString,
schema.SystemMessage("你是一个专业的商品参数解析器,你需要根据用户输入的商品描述,解析出商品的目标参数。"),
schema.SystemMessage("目标参数:"+constants.HYTProductPropertyTemplate),
schema.UserMessage("用户输入:{{.Text}}"),
))
// 2. 调用LLM
c.AppendChatModel(paramMappingModel)
// 3.工具参数整理
c.AppendLambda(compose.InvokableLambda(func(_ context.Context, in *schema.Message) (*toolHytPu.ProductUploadRequest, error) {
toolReq := &toolHytPu.ProductUploadRequest{}
if err := json.Unmarshal([]byte(in.Content), toolReq); err != nil {
return nil, err
}
return toolReq, nil
}))
// 4.工具调用
c.AppendLambda(compose.InvokableLambda(func(_ context.Context, in *toolHytPu.ProductUploadRequest) (*toolHytPu.ProductUploadResponse, error) {
toolRes, err := toolHytPu.Call(ctx, o.cfg.Tools.HytProductUpload, in)
return toolRes, err
}))
// 5.结果数据映射
c.AppendLambda(compose.InvokableLambda(func(_ context.Context, in *toolHytPu.ProductUploadResponse) (map[string]any, error) {
return map[string]any{
"预览URL(货易通商品列表)": in.PreviewUrl,
"SPU编码": in.SpuNum,
"商品ID": in.Id,
}, nil
}))
// 6.编译工作流
return c.Compile(ctx)
}

View File

@ -11,7 +11,7 @@ import (
type Workflow interface {
ID() string
Schema() map[string]any
// Schema() map[string]any
Invoke(ctx context.Context, requireData *entitys.Recognize) (map[string]any, error)
}

View File

@ -79,13 +79,13 @@ type OrderAfterSaleResellerBatchData struct {
func (o *orderAfterSaleResellerBatch) ID() string { return "zltx.orderAfterSaleResellerBatch" }
// Schema 返回入参约束(用于校验/表单生成)
func (o *orderAfterSaleResellerBatch) Schema() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{"orderNumber": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}},
"required": []string{"orderNumber"},
}
}
// func (o *orderAfterSaleResellerBatch) Schema() map[string]any {
// return map[string]any{
// "type": "object",
// "properties": map[string]any{"orderNumber": map[string]any{"type": "array", "items": map[string]any{"type": "string"}}},
// "required": []string{"orderNumber"},
// }
// }
// Invoke 调用原有编排工作流并规范化输出
func (o *orderAfterSaleResellerBatch) Invoke(ctx context.Context, rec *entitys.Recognize) (map[string]any, error) {

14
internal/pkg/util/map.go Normal file
View File

@ -0,0 +1,14 @@
package util
import "encoding/json"
// StructToMap 将结构体转换为 map[string]any
func StructToMap(v any) (map[string]any, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
var m map[string]any
err = json.Unmarshal(b, &m)
return m, err
}

View File

@ -34,6 +34,11 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
c.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// AI能力调用路由设置不同的 CORS 头
if strings.HasPrefix(c.Path(), "/api/v1/capability") {
c.Set("Access-Control-Allow-Headers", "Content-Type, X-Source-Key, X-Timestamp")
}
// 如果是预检请求OPTIONS直接返回 204
if c.Method() == "OPTIONS" {
return c.SendStatus(fiber.StatusNoContent) // 204
@ -88,7 +93,8 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
r.Post("/chat/history/update/content", chatHist.UpdateContent)
// 能力
r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取
r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取
r.Post("/capability/product/upload/hyt", capabilityService.ProductUploadHyt) // 货易通商品数据上传
}
func routerSocket(app *fiber.App, chatService *services.ChatService) {

View File

@ -2,7 +2,10 @@ package services
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
errorcode "ai_scheduler/internal/data/error"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
@ -16,12 +19,14 @@ import (
// CapabilityService 统一回调入口
type CapabilityService struct {
cfg *config.Config
cfg *config.Config
workflowManager *runtime.Registry
}
func NewCapabilityService(cfg *config.Config) *CapabilityService {
func NewCapabilityService(cfg *config.Config, workflowManager *runtime.Registry) *CapabilityService {
return &CapabilityService{
cfg: cfg,
cfg: cfg,
workflowManager: workflowManager,
}
}
@ -34,56 +39,11 @@ type ProductIngestReq struct {
Timestamp int64 `json:"timestamp"` // 商品发布时间戳
}
const (
// 货易通商品属性模板-中文
HYTProductPropertyTemplateZH = `{
"条码": "string", // 商品编号
"分类名称": "string", // 商品分类
"货品名称": "string", // 商品名称
"货品编号": "string", // 商品编号
"商品货号": "string", // 商品编号
"品牌": "string", // 商品品牌
"单位": "string", // 商品单位,若无则使用'个'
"规格参数": "string", // 商品规格参数
"货品说明": "string", // 商品说明
"保质期": "string", // 商品保质期
"保质期单位": "string", // 商品保质期单位
"链接": "string", //
"货品图片": ["string"], // 商品多图取1-2个即可
"电商销售价格": "decimal(10,2)", // 商品电商销售价格
"销售价": "decimal(10,2)", // 商品销售价格
"供应商报价": "decimal(10,2)", // 商品供应商报价
"税率": "number%", // 商品税率 x%
"默认供应商": "", // 空即可
"默认存放仓库": "", // 空即可
"备注": "", // 备注
"长": "string", // 商品长度decimal(10,2)+单位
"宽": "string", // 商品宽度decimal(10,2)+单位
"高": "string", // 商品高度decimal(10,2)+单位
"重量": "string", // 商品重量(kg)
"SPU名称": "string", // 商品SPU名称
"SPU编码": "string" // 编码串jd_{timestamp}_rand(1000-999)
}`
SystemPrompt = `你是一个专业的商品属性提取助手你的任务是根据用户输入提取商品的属性信息
目标属性模板%s
最终输出格式为纯JSON字符串键值对对应目标属性和提取到的属性值
最终输出不要携带markdown标识不要携带回车换行`
)
// ProductIngest 产品数据提取
func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
// 读取头
token := strings.TrimSpace(c.Get("X-Source-Key"))
ts := strings.TrimSpace(c.Get("X-Timestamp"))
// 时间窗口校验
if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) {
return errorcode.AuthNotFound
}
// token校验
if token == "" || token != "A7f9KQ3mP2X8LZC4R5e" {
return errorcode.KeyErr()
// 请求头校验
if err := s.checkRequestHeader(c); err != nil {
return err
}
// 解析请求参数
@ -91,7 +51,6 @@ func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
if err := c.BodyParser(&req); err != nil {
return errorcode.ParamErr("invalid request body: %v", err)
}
// 必要参数校验
if req.Text == "" {
return errorcode.ParamErr("missing required fields")
@ -107,7 +66,7 @@ func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
res, err := client.Chat(context.Background(), []api.Message{
{
Role: "system",
Content: fmt.Sprintf(SystemPrompt, HYTProductPropertyTemplateZH),
Content: fmt.Sprintf(constants.SystemPrompt, constants.HYTProductPropertyTemplateZH),
},
{
Role: "user",
@ -122,8 +81,50 @@ func (s *CapabilityService) ProductIngest(c *fiber.Ctx) error {
return err
}
// res.Message.Content Go中map会无序交给前端解析
// 解析模型输出
c.JSON(res.Message.Content)
return nil
}
// checkRequestHeader 校验请求头
func (s *CapabilityService) checkRequestHeader(c *fiber.Ctx) error {
// 读取头
token := strings.TrimSpace(c.Get("X-Source-Key"))
ts := strings.TrimSpace(c.Get("X-Timestamp"))
// 时间窗口校验
if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) {
return errorcode.AuthNotFound
}
// token校验
if token == "" || token != "A7f9KQ3mP2X8LZC4R5e" {
return errorcode.KeyErr()
}
return nil
}
// ProductUploadHyt 商品上传至货易通
func (s *CapabilityService) ProductUploadHyt(c *fiber.Ctx) error {
// 请求头校验
if err := s.checkRequestHeader(c); err != nil {
return err
}
// 获取 body json 串
raw := append([]byte(nil), c.BodyRaw()...)
bodyStr := string(raw)
// 调用eino工作流实现商品上传到货易通
workflowId := "hyt.productUpload"
rec := &entitys.Recognize{UserContent: &entitys.RecognizeUserContent{Text: bodyStr}}
res, err := s.workflowManager.Invoke(context.Background(), workflowId, rec)
if err != nil {
return err
}
return c.JSON(res)
}