feat: batch AI match SYB purchase specs (#188)
This commit is contained in:
@@ -97,6 +97,7 @@ var AdminAPIs = []APIPermission{
|
||||
|
||||
{"查看采购任务", "/api/admin/v1/purchase-tasks", "GET", true},
|
||||
{"预检批量采购", "/api/admin/v1/purchase-tasks/batch-preview", "POST", true},
|
||||
{"批量 AI 匹配采购规格", "/api/admin/v1/purchase-tasks/batch-spec-match", "POST", true},
|
||||
{"批量创建采购任务", "/api/admin/v1/purchase-tasks/batch", "POST", true},
|
||||
{"批量重试采购任务", "/api/admin/v1/purchase-tasks/batch-retry", "POST", true},
|
||||
{"创建备货采购任务", "/api/admin/v1/purchase-tasks/stock", "POST", true},
|
||||
|
||||
@@ -53,3 +53,12 @@ func TestPurchaserMayCreateStockPurchase(t *testing.T) {
|
||||
}
|
||||
t.Fatal("missing purchaser stock purchase permission")
|
||||
}
|
||||
|
||||
func TestPurchaserMayRunBatchSpecMatch(t *testing.T) {
|
||||
for _, permission := range PurchaserAPIs() {
|
||||
if permission.Method == "POST" && permission.Path == "/api/admin/v1/purchase-tasks/batch-spec-match" {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("missing purchaser batch spec match permission")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package purchase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go-admin/app/goauto/aimatching"
|
||||
"go-admin/app/goauto/purchaserule"
|
||||
"go-admin/app/goauto/shopeeproduct"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
BatchSpecMatchAutoConfirmed = "auto_confirmed"
|
||||
BatchSpecMatchPending = "pending"
|
||||
BatchSpecMatchFailed = "failed"
|
||||
BatchSpecMatchSkipped = "skipped"
|
||||
)
|
||||
|
||||
type BatchSpecMatchRequest struct {
|
||||
SYBProductIDs []uint64 `json:"sybProductIds"`
|
||||
}
|
||||
|
||||
type BatchSpecMatchItem struct {
|
||||
SYBProductID uint64 `json:"sybProductId"`
|
||||
OrderCode string `json:"orderCode,omitempty"`
|
||||
ShopeeProductID *uint64 `json:"shopeeProductId,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Confidence *float64 `json:"confidence,omitempty"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type BatchSpecMatchResponse struct {
|
||||
Items []BatchSpecMatchItem `json:"items"`
|
||||
AutoConfirmedCount int `json:"autoConfirmedCount"`
|
||||
PendingCount int `json:"pendingCount"`
|
||||
FailedCount int `json:"failedCount"`
|
||||
SkippedCount int `json:"skippedCount"`
|
||||
}
|
||||
|
||||
func (s *Service) BatchSpecMatch(ctx context.Context, req BatchSpecMatchRequest) (BatchSpecMatchResponse, error) {
|
||||
ids, err := validateBatchIDs(req.SYBProductIDs)
|
||||
if err != nil {
|
||||
return BatchSpecMatchResponse{}, err
|
||||
}
|
||||
dataset, err := s.loadBatchPreviewDataset(ctx, ids)
|
||||
if err != nil {
|
||||
return BatchSpecMatchResponse{}, internal(err)
|
||||
}
|
||||
guard, ruleReason, err := s.readOnlyPriceGuard(ctx)
|
||||
if err != nil {
|
||||
return BatchSpecMatchResponse{}, err
|
||||
}
|
||||
settings, err := aimatching.NewService(s.DB).Settings(ctx)
|
||||
if err != nil {
|
||||
return BatchSpecMatchResponse{}, internal(err)
|
||||
}
|
||||
|
||||
response := BatchSpecMatchResponse{Items: make([]BatchSpecMatchItem, 0, len(ids))}
|
||||
for _, id := range ids {
|
||||
item := BatchSpecMatchItem{SYBProductID: id, Status: BatchSpecMatchFailed}
|
||||
syb, found := dataset.sybByID[id]
|
||||
if found {
|
||||
item.OrderCode, item.ShopeeProductID = syb.OrderCode, syb.ShopeeProductID
|
||||
}
|
||||
preview := s.previewFromDataset(ctx, id, dataset, false, guard)
|
||||
if ruleReason != "" && preview.Eligible {
|
||||
preview.Eligible, preview.ReasonCode, preview.Reason, preview.NextAction = false, purchaserule.CodeCurrentRuleMissing, ruleReason, "refresh"
|
||||
}
|
||||
stage := processStageFromDataset(id, dataset, preview)
|
||||
if stage.Stage != ProcessStageColorMapping {
|
||||
item.Status, item.Reason = BatchSpecMatchSkipped, "当前不是颜色待匹配阶段,无需处理"
|
||||
response.SkippedCount++
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
}
|
||||
|
||||
shopee := dataset.shopeeByID[*syb.ShopeeProductID]
|
||||
pdd := dataset.pddByID[*shopee.PDDProductID]
|
||||
candidates, usable := archiveCandidates(pdd.SpecsJSON, syb.TargetColor, syb.TargetSize)
|
||||
if !usable {
|
||||
item.Reason = "关联的 PDD 商品没有完整可选规格"
|
||||
response.FailedCount++
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
}
|
||||
mappedColor, mappedSize, _ := confirmedMappings(shopee.SpecsJSON, syb.TargetColor, syb.TargetSize)
|
||||
if mappedColor != "" && !containsValue(candidates.Colors, mappedColor) {
|
||||
mappedColor = ""
|
||||
}
|
||||
if mappedSize != "" && !containsValue(candidates.Sizes, mappedSize) {
|
||||
mappedSize = ""
|
||||
}
|
||||
request := aimatching.MatchRequest{Colors: candidates.Colors, Sizes: candidates.Sizes}
|
||||
if mappedColor == "" {
|
||||
request.TargetColor = syb.TargetColor
|
||||
}
|
||||
if mappedSize == "" {
|
||||
request.TargetSize = syb.TargetSize
|
||||
}
|
||||
matched, matchErr := s.matcher().Resolve(ctx, request)
|
||||
if matchErr != nil {
|
||||
item.Reason = batchSpecMatchErrorMessage(matchErr)
|
||||
response.FailedCount++
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
}
|
||||
item.Source, item.Confidence = matched.Source, matched.Decision.Confidence
|
||||
autoConfirm := matched.Source == aimatching.SourceAI && matched.Decision.Confidence != nil && *matched.Decision.Confidence >= settings.AutoConfirmMinConfidence && strings.TrimSpace(matched.Decision.Reason) != ""
|
||||
if !autoConfirm {
|
||||
item.Status, item.Reason = BatchSpecMatchPending, "匹配结果未达到自动确认阈值,请人工确认"
|
||||
if strings.TrimSpace(matched.Decision.Reason) != "" {
|
||||
item.Reason += ":" + strings.TrimSpace(matched.Decision.Reason)
|
||||
}
|
||||
response.PendingCount++
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
}
|
||||
writes, buildErr := resolvedMappingWrites(shopee.SpecsJSON, request, matched, shopeeproduct.MappingStatusConfirmed)
|
||||
if buildErr != nil {
|
||||
item.Reason = buildErr.Error()
|
||||
response.FailedCount++
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
}
|
||||
if _, err := shopeeproduct.NewService(s.DB).ApplyResolvedMappings(ctx, shopee.ID, uuid.NewString(), settings.AutoConfirmMinConfidence, writes); err != nil {
|
||||
item.Reason = "规格映射保存失败,请刷新后重试"
|
||||
response.FailedCount++
|
||||
response.Items = append(response.Items, item)
|
||||
continue
|
||||
}
|
||||
if err := s.DB.WithContext(ctx).First(&shopee, shopee.ID).Error; err == nil {
|
||||
dataset.shopeeByID[shopee.ID] = shopee
|
||||
}
|
||||
item.Status, item.Reason = BatchSpecMatchAutoConfirmed, strings.TrimSpace(matched.Decision.Reason)
|
||||
response.AutoConfirmedCount++
|
||||
response.Items = append(response.Items, item)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func resolvedMappingWrites(raw string, request aimatching.MatchRequest, matched aimatching.MatchResult, status string) ([]shopeeproduct.ResolvedMappingItem, error) {
|
||||
specs, err := shopeeproduct.Unmarshal(raw)
|
||||
if err != nil {
|
||||
return nil, errors.New("蝦皮商品规格数据无效")
|
||||
}
|
||||
writes := make([]shopeeproduct.ResolvedMappingItem, 0, 2)
|
||||
add := func(role, target, pddValue string) error {
|
||||
if strings.TrimSpace(target) == "" {
|
||||
return nil
|
||||
}
|
||||
for _, dimension := range specs {
|
||||
if dimension.Role != role {
|
||||
continue
|
||||
}
|
||||
for _, value := range dimension.Values {
|
||||
if value.Name == target {
|
||||
writes = append(writes, shopeeproduct.ResolvedMappingItem{Dimension: dimension.Name, ValueName: target, PDDValue: pddValue, Source: matched.Source, Status: status, Confidence: matched.Decision.Confidence, Reason: matched.Decision.Reason})
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("蝦皮商品中未找到目标规格“%s”", target)
|
||||
}
|
||||
if err := add(shopeeproduct.RoleColor, request.TargetColor, matched.MappedColor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := add(shopeeproduct.RoleSize, request.TargetSize, matched.MappedSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(writes) == 0 {
|
||||
return nil, errors.New("没有需要写入的规格映射")
|
||||
}
|
||||
return writes, nil
|
||||
}
|
||||
|
||||
func batchSpecMatchErrorMessage(err error) string {
|
||||
var matchErr *aimatching.Error
|
||||
if errors.As(err, &matchErr) {
|
||||
switch matchErr.Code {
|
||||
case aimatching.CodeNotConfigured:
|
||||
return "AI 匹配未启用或未配置,请先在设置中启用"
|
||||
case aimatching.CodeNoMatch:
|
||||
return "AI 未找到可靠的 PDD 规格"
|
||||
case aimatching.CodeProviderUnavailable:
|
||||
return "AI 匹配服务暂时不可用,请稍后重试"
|
||||
case aimatching.CodeInvalidSetting:
|
||||
return "AI 匹配设置无效,请检查配置"
|
||||
}
|
||||
}
|
||||
return "规格匹配失败,请稍后重试"
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package purchase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go-admin/app/goauto/aimatching"
|
||||
"go-admin/app/goauto/models"
|
||||
"go-admin/app/goauto/shopeeproduct"
|
||||
)
|
||||
|
||||
type batchSpecMatcher struct {
|
||||
results []aimatching.MatchResult
|
||||
errs []error
|
||||
calls int
|
||||
}
|
||||
|
||||
func (matcher *batchSpecMatcher) Resolve(_ context.Context, _ aimatching.MatchRequest) (aimatching.MatchResult, error) {
|
||||
at := matcher.calls
|
||||
matcher.calls++
|
||||
if at < len(matcher.errs) && matcher.errs[at] != nil {
|
||||
return aimatching.MatchResult{}, matcher.errs[at]
|
||||
}
|
||||
return matcher.results[at], nil
|
||||
}
|
||||
|
||||
func unresolvedBatchSpecFixture(t *testing.T) (*Service, fixture) {
|
||||
t.Helper()
|
||||
db := testDB(t)
|
||||
f := seed(t, db, liveCaps(), true)
|
||||
pddSpecs := `[{"name":"颜色","role":"color","values":[{"name":"米白色","selectable":true,"priceCent":2000},{"name":"白色","selectable":true,"priceCent":2000}]},{"name":"尺码","role":"size","values":[{"name":"XL","selectable":true}]}]`
|
||||
if err := db.Model(&models.PDDProduct{}).Where("id = ?", f.pdd.ID).Update("specs_json", pddSpecs).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
shopeeSpecs := `[{"name":"颜色分类","role":"color","values":[{"name":"象牙白","source":"import","mapping":{"pddValue":"旧白色","source":"manual","status":"confirmed"}}]},{"name":"尺码","role":"size","values":[{"name":"XL","source":"import","mapping":{"pddValue":"XL","source":"manual","status":"confirmed"}}]}]`
|
||||
if err := db.Model(&models.ShopeeProduct{}).Where("id = ?", f.shopee.ID).Update("specs_json", shopeeSpecs).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Model(&models.SYBProduct{}).Where("id = ?", f.syb.ID).Update("target_color", "象牙白").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.syb.TargetColor = "象牙白"
|
||||
return testService(db), f
|
||||
}
|
||||
|
||||
func aiBatchResult(confidence float64) aimatching.MatchResult {
|
||||
request := aimatching.MatchRequest{TargetColor: "象牙白", Colors: []string{"米白色", "白色"}, Sizes: []string{"XL"}}
|
||||
result := aimatching.RecordedMatch(request, aimatching.SourceAI, "米白色", "", "颜色语义与候选唯一对应")
|
||||
result.Decision.Confidence = &confidence
|
||||
return result
|
||||
}
|
||||
|
||||
func savedColorMapping(t *testing.T, service *Service, productID uint64) *shopeeproduct.Mapping {
|
||||
t.Helper()
|
||||
var product models.ShopeeProduct
|
||||
if err := service.DB.First(&product, productID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
specs, err := shopeeproduct.Unmarshal(product.SpecsJSON)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return specs[0].Values[0].Mapping
|
||||
}
|
||||
|
||||
func TestBatchSpecMatchAutoConfirmsHighConfidenceAndRefreshesReadiness(t *testing.T) {
|
||||
service, f := unresolvedBatchSpecFixture(t)
|
||||
duplicate := f.syb
|
||||
duplicate.ID = 0
|
||||
duplicate.OrderCode = "SYB-2"
|
||||
duplicate.DetailID++
|
||||
if err := service.DB.Create(&duplicate).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
matcher := &batchSpecMatcher{results: []aimatching.MatchResult{aiBatchResult(0.95)}}
|
||||
service.Matcher = matcher
|
||||
|
||||
response, err := service.BatchSpecMatch(context.Background(), BatchSpecMatchRequest{SYBProductIDs: []uint64{f.syb.ID, duplicate.ID}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.AutoConfirmedCount != 1 || response.SkippedCount != 1 || matcher.calls != 1 {
|
||||
t.Fatalf("unexpected mixed result: %+v calls=%d", response, matcher.calls)
|
||||
}
|
||||
mapping := savedColorMapping(t, service, f.shopee.ID)
|
||||
if mapping == nil || mapping.Status != shopeeproduct.MappingStatusConfirmed || mapping.Source != shopeeproduct.MappingSourceAIMatch || mapping.Confidence == nil || *mapping.Confidence != 0.95 {
|
||||
t.Fatalf("high-confidence mapping was not independently confirmed: %+v", mapping)
|
||||
}
|
||||
preview, err := service.BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{f.syb.ID}})
|
||||
if err != nil || len(preview.Items) != 1 || !preview.Items[0].Eligible || preview.Items[0].ProcessStage != ProcessStagePurchaseReady {
|
||||
t.Fatalf("readiness did not reflect confirmed mapping: %+v err=%v", preview, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchSpecMatchLeavesLowConfidenceForManualHandling(t *testing.T) {
|
||||
service, f := unresolvedBatchSpecFixture(t)
|
||||
service.Matcher = &batchSpecMatcher{results: []aimatching.MatchResult{aiBatchResult(0.6)}}
|
||||
|
||||
response, err := service.BatchSpecMatch(context.Background(), BatchSpecMatchRequest{SYBProductIDs: []uint64{f.syb.ID}})
|
||||
if err != nil || response.PendingCount != 1 || response.AutoConfirmedCount != 0 {
|
||||
t.Fatalf("unexpected low-confidence result: %+v err=%v", response, err)
|
||||
}
|
||||
mapping := savedColorMapping(t, service, f.shopee.ID)
|
||||
if mapping == nil || mapping.PDDValue != "旧白色" || mapping.Status != shopeeproduct.MappingStatusConfirmed {
|
||||
t.Fatalf("low-confidence result changed the saved mapping: %+v", mapping)
|
||||
}
|
||||
preview, err := service.BatchPreview(context.Background(), BatchPreviewRequest{SYBProductIDs: []uint64{f.syb.ID}})
|
||||
if err != nil || preview.Items[0].Eligible || preview.Items[0].ProcessStage != ProcessStageColorMapping {
|
||||
t.Fatalf("pending mapping unexpectedly became purchase-ready: %+v err=%v", preview, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchSpecMatchReportsProviderFailureWithoutWriting(t *testing.T) {
|
||||
service, f := unresolvedBatchSpecFixture(t)
|
||||
service.Matcher = &batchSpecMatcher{errs: []error{&aimatching.Error{Code: aimatching.CodeProviderUnavailable, Message: "provider down"}}}
|
||||
|
||||
response, err := service.BatchSpecMatch(context.Background(), BatchSpecMatchRequest{SYBProductIDs: []uint64{f.syb.ID}})
|
||||
if err != nil || response.FailedCount != 1 || response.Items[0].Status != BatchSpecMatchFailed {
|
||||
t.Fatalf("unexpected provider failure result: %+v err=%v", response, err)
|
||||
}
|
||||
if mapping := savedColorMapping(t, service, f.shopee.ID); mapping == nil || mapping.PDDValue != "旧白色" || mapping.Status != shopeeproduct.MappingStatusConfirmed {
|
||||
t.Fatalf("provider failure changed the existing mapping: %+v", mapping)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchSpecMatchErrorMessageDoesNotExposeProviderCause(t *testing.T) {
|
||||
err := &aimatching.Error{Code: aimatching.CodeProviderUnavailable, Message: "safe", Cause: errors.New("secret upstream detail")}
|
||||
if got := batchSpecMatchErrorMessage(err); got != "AI 匹配服务暂时不可用,请稍后重试" {
|
||||
t.Fatalf("unsafe or unexpected message: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,26 @@ func (h Handler) AdminBatchPreview(c *gin.Context) {
|
||||
writeAdminData(c, response)
|
||||
}
|
||||
|
||||
func (h Handler) AdminBatchSpecMatch(c *gin.Context) {
|
||||
if !allowedOperator(c) {
|
||||
return
|
||||
}
|
||||
var req BatchSpecMatchRequest
|
||||
if !decode(c, &req) {
|
||||
return
|
||||
}
|
||||
service, ok := h.service(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
response, err := service.BatchSpecMatch(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
}
|
||||
writeAdminData(c, response)
|
||||
}
|
||||
|
||||
func (h Handler) AdminBatchCreate(c *gin.Context) {
|
||||
if !allowedOperator(c) {
|
||||
return
|
||||
|
||||
@@ -28,6 +28,7 @@ func InitRouter(engine *gin.Engine, auth *jwt.GinJWTMiddleware) {
|
||||
admin := engine.Group("/api/admin/v1/purchase-tasks").Use(auth.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||
admin.GET("", h.AdminList)
|
||||
admin.POST("/batch-preview", h.AdminBatchPreview)
|
||||
admin.POST("/batch-spec-match", h.AdminBatchSpecMatch)
|
||||
admin.POST("/batch", h.AdminBatchCreate)
|
||||
admin.POST("/batch-retry", h.AdminBatchRetry)
|
||||
admin.POST("/stock", h.AdminCreateStock)
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package shopeeproduct
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ResolvedMappingItem is one mapping produced by the narrowly scoped SYB
|
||||
// batch-match entry point. Confirmed writes are intentionally restricted to
|
||||
// auditable AI decisions; the general SetMapping path remains pending-first.
|
||||
type ResolvedMappingItem struct {
|
||||
Dimension string
|
||||
ValueName string
|
||||
PDDValue string
|
||||
Source string
|
||||
Status string
|
||||
Confidence *float64
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ApplyResolvedMappings atomically applies the color/size mappings needed by
|
||||
// one SYB detail row. It is independent from SetMapping so #188's explicit
|
||||
// high-confidence exception cannot change existing callers' pending semantics.
|
||||
func (service *Service) ApplyResolvedMappings(ctx context.Context, id uint64, requestID string, minimumConfidence float64, items []ResolvedMappingItem) (SaveResponse, error) {
|
||||
if len(items) == 0 || len(items) > 2 {
|
||||
return SaveResponse{}, invalidRequest("必须包含 1 至 2 个待写入规格映射")
|
||||
}
|
||||
if minimumConfidence < 0 || minimumConfidence > 1 {
|
||||
return SaveResponse{}, invalidRequest("自动确认阈值无效")
|
||||
}
|
||||
seen := make(map[string]bool, len(items))
|
||||
for _, item := range items {
|
||||
key := strings.TrimSpace(item.Dimension) + "\x00" + strings.TrimSpace(item.ValueName)
|
||||
if seen[key] {
|
||||
return SaveResponse{}, invalidRequest("待写入规格映射重复")
|
||||
}
|
||||
seen[key] = true
|
||||
if item.Source != MappingSourceExactMatch && item.Source != MappingSourceAIMatch {
|
||||
return SaveResponse{}, invalidRequest("批量匹配来源无效")
|
||||
}
|
||||
if item.Status != MappingStatusConfirmed || item.Source != MappingSourceAIMatch || item.Confidence == nil || *item.Confidence < minimumConfidence || strings.TrimSpace(item.Reason) == "" {
|
||||
return SaveResponse{}, invalidRequest("自动确认仅允许带置信度和理由的 AI 匹配")
|
||||
}
|
||||
if err := service.validatePDDMappingTarget(ctx, id, item.Dimension, item.ValueName, strings.TrimSpace(item.PDDValue)); err != nil {
|
||||
return SaveResponse{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return service.mutateSpecs(ctx, id, requestID, func(specs []SpecDimension) ([]SpecDimension, error) {
|
||||
for _, item := range items {
|
||||
updated := false
|
||||
for di, dimension := range specs {
|
||||
if dimension.Name != item.Dimension {
|
||||
continue
|
||||
}
|
||||
for vi, value := range dimension.Values {
|
||||
if value.Name != item.ValueName {
|
||||
continue
|
||||
}
|
||||
specs[di].Values[vi].Mapping = &Mapping{
|
||||
PDDValue: strings.TrimSpace(item.PDDValue), Source: item.Source, Status: item.Status,
|
||||
Confidence: item.Confidence, Reason: strings.TrimSpace(item.Reason),
|
||||
}
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
if !updated {
|
||||
return nil, valueNotFound()
|
||||
}
|
||||
}
|
||||
return specs, nil
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user