feat(#115): move PDD recognition aliases into rules
This commit is contained in:
@@ -8,6 +8,8 @@ import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go-admin/app/goauto/purchasecontract"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -63,23 +65,69 @@ type hookAction struct {
|
||||
}
|
||||
|
||||
type collectorConfig struct {
|
||||
CollectorID string `json:"collectorId"`
|
||||
SpecEntryStrategy string `json:"specEntryStrategy"`
|
||||
PriceParser string `json:"priceParser"`
|
||||
PriceGranularity string `json:"priceGranularity"`
|
||||
DimensionAliases map[string][]string `json:"dimensionAliases"`
|
||||
TimeoutsMS map[string]int `json:"timeoutsMs"`
|
||||
Limits map[string]int `json:"limits"`
|
||||
CollectorID string `json:"collectorId"`
|
||||
SpecEntryStrategy string `json:"specEntryStrategy"`
|
||||
PriceParser string `json:"priceParser"`
|
||||
PriceGranularity string `json:"priceGranularity"`
|
||||
DimensionAliases map[string][]string `json:"dimensionAliases"`
|
||||
TimeoutsMS map[string]int `json:"timeoutsMs"`
|
||||
Limits map[string]int `json:"limits"`
|
||||
TextAliases *collectorTextAliases `json:"textAliases,omitempty"`
|
||||
}
|
||||
|
||||
type collectorTextAliases struct {
|
||||
SpecPanel specPanelTextAliases `json:"specPanel"`
|
||||
Dimension dimensionTextAliases `json:"dimension"`
|
||||
Selection selectionTextAliases `json:"selection"`
|
||||
Review reviewTextAliases `json:"review"`
|
||||
SoldOut soldOutTextAliases `json:"soldOut"`
|
||||
Purchase purchaseTextAliases `json:"purchase"`
|
||||
}
|
||||
|
||||
type specPanelTextAliases struct {
|
||||
TitleAliases []string `json:"titleAliases"`
|
||||
CloseAliases []string `json:"closeAliases"`
|
||||
ConfirmAliases []string `json:"confirmAliases"`
|
||||
QuantityDecreaseAliases []string `json:"quantityDecreaseAliases"`
|
||||
QuantityIncreaseAliases []string `json:"quantityIncreaseAliases"`
|
||||
PaymentAreaAliases []string `json:"paymentAreaAliases"`
|
||||
OrderSubmitAliases []string `json:"orderSubmitAliases"`
|
||||
QuickBuyAliases []string `json:"quickBuyAliases"`
|
||||
}
|
||||
|
||||
type dimensionTextAliases struct {
|
||||
ColorSuffixes []string `json:"colorSuffixes"`
|
||||
SizeSuffixes []string `json:"sizeSuffixes"`
|
||||
ExactNames []string `json:"exactNames"`
|
||||
AdaptiveAliases []string `json:"adaptiveAliases"`
|
||||
}
|
||||
|
||||
type selectionTextAliases struct {
|
||||
SummaryPrefixes []string `json:"summaryPrefixes"`
|
||||
SelectedPrefixes []string `json:"selectedPrefixes"`
|
||||
SpecEntryPrefixes []string `json:"specEntryPrefixes"`
|
||||
SubmitHintAliases []string `json:"submitHintAliases"`
|
||||
}
|
||||
|
||||
type reviewTextAliases struct {
|
||||
EntryAliases []string `json:"entryAliases"`
|
||||
}
|
||||
type soldOutTextAliases struct {
|
||||
MainProductExactAliases []string `json:"mainProductExactAliases"`
|
||||
MainProductPrefixAliases []string `json:"mainProductPrefixAliases"`
|
||||
}
|
||||
type purchaseTextAliases struct {
|
||||
BuyWords []string `json:"buyWords"`
|
||||
}
|
||||
|
||||
type transientSoldOutRecovery struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ExactText string `json:"exactText"`
|
||||
FallbackTopText string `json:"fallbackTopText,omitempty"`
|
||||
PullDownCount int `json:"pullDownCount"`
|
||||
IntervalMS int `json:"intervalMs"`
|
||||
SettleMS int `json:"settleMs"`
|
||||
MaxAttempts int `json:"maxAttempts"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ExactText string `json:"exactText"`
|
||||
FallbackTopText *string `json:"fallbackTopText,omitempty"`
|
||||
PullDownCount int `json:"pullDownCount"`
|
||||
IntervalMS int `json:"intervalMs"`
|
||||
SettleMS int `json:"settleMs"`
|
||||
MaxAttempts int `json:"maxAttempts"`
|
||||
}
|
||||
|
||||
type pageRecovery struct {
|
||||
@@ -260,11 +308,13 @@ func parseV2(raw []byte) (v2Rule, error) {
|
||||
}
|
||||
if rule.PageRecovery != nil && rule.PageRecovery.TransientSoldOut != nil {
|
||||
recovery := rule.PageRecovery.TransientSoldOut
|
||||
if recovery.ExactText != "商品已售罄" {
|
||||
return rule, errors.New("pageRecovery.transientSoldOut.exactText 只能是商品已售罄")
|
||||
if err := purchasecontract.ValidateReadOnlyTextAliases("pageRecovery.transientSoldOut.exactText", []string{recovery.ExactText}, 1, 30); err != nil {
|
||||
return rule, err
|
||||
}
|
||||
if recovery.FallbackTopText != "" && recovery.FallbackTopText != "相似商品" {
|
||||
return rule, errors.New("pageRecovery.transientSoldOut.fallbackTopText 只能是相似商品")
|
||||
if recovery.FallbackTopText != nil {
|
||||
if err := purchasecontract.ValidateReadOnlyTextAliases("pageRecovery.transientSoldOut.fallbackTopText", []string{*recovery.FallbackTopText}, 1, 30); err != nil {
|
||||
return rule, err
|
||||
}
|
||||
}
|
||||
if recovery.PullDownCount < 1 || recovery.PullDownCount > 3 {
|
||||
return rule, errors.New("pageRecovery.transientSoldOut.pullDownCount 必须为 1..3")
|
||||
@@ -306,12 +356,39 @@ func parseV2(raw []byte) (v2Rule, error) {
|
||||
}
|
||||
for _, key := range []string{"color", "size"} {
|
||||
aliases := rule.Collector.DimensionAliases[key]
|
||||
if len(aliases) == 0 || len(aliases) > 20 {
|
||||
return rule, fmt.Errorf("dimensionAliases.%s 必须包含 1..20 项", key)
|
||||
if err := validateAliases(aliases, "dimensionAliases."+key, 1, 20, 30); err != nil {
|
||||
return rule, err
|
||||
}
|
||||
for _, alias := range aliases {
|
||||
if strings.TrimSpace(alias) == "" || len([]rune(alias)) > 30 {
|
||||
return rule, fmt.Errorf("dimensionAliases.%s 含无效别名", key)
|
||||
}
|
||||
if aliases := rule.Collector.TextAliases; aliases != nil {
|
||||
groups := []struct {
|
||||
name string
|
||||
values []string
|
||||
}{
|
||||
{"specPanel.titleAliases", aliases.SpecPanel.TitleAliases},
|
||||
{"specPanel.closeAliases", aliases.SpecPanel.CloseAliases},
|
||||
{"specPanel.confirmAliases", aliases.SpecPanel.ConfirmAliases},
|
||||
{"specPanel.quantityDecreaseAliases", aliases.SpecPanel.QuantityDecreaseAliases},
|
||||
{"specPanel.quantityIncreaseAliases", aliases.SpecPanel.QuantityIncreaseAliases},
|
||||
{"specPanel.paymentAreaAliases", aliases.SpecPanel.PaymentAreaAliases},
|
||||
{"specPanel.orderSubmitAliases", aliases.SpecPanel.OrderSubmitAliases},
|
||||
{"specPanel.quickBuyAliases", aliases.SpecPanel.QuickBuyAliases},
|
||||
{"dimension.colorSuffixes", aliases.Dimension.ColorSuffixes},
|
||||
{"dimension.sizeSuffixes", aliases.Dimension.SizeSuffixes},
|
||||
{"dimension.exactNames", aliases.Dimension.ExactNames},
|
||||
{"dimension.adaptiveAliases", aliases.Dimension.AdaptiveAliases},
|
||||
{"selection.summaryPrefixes", aliases.Selection.SummaryPrefixes},
|
||||
{"selection.selectedPrefixes", aliases.Selection.SelectedPrefixes},
|
||||
{"selection.specEntryPrefixes", aliases.Selection.SpecEntryPrefixes},
|
||||
{"selection.submitHintAliases", aliases.Selection.SubmitHintAliases},
|
||||
{"review.entryAliases", aliases.Review.EntryAliases},
|
||||
{"soldOut.mainProductExactAliases", aliases.SoldOut.MainProductExactAliases},
|
||||
{"soldOut.mainProductPrefixAliases", aliases.SoldOut.MainProductPrefixAliases},
|
||||
{"purchase.buyWords", aliases.Purchase.BuyWords},
|
||||
}
|
||||
for _, group := range groups {
|
||||
if err := purchasecontract.ValidateReadOnlyTextAliases("collector.textAliases."+group.name, group.values, 20, 30); err != nil {
|
||||
return rule, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,9 @@ func TestBuiltInPDDProductDetailTemplateIsValid(t *testing.T) {
|
||||
if !bytes.Contains(raw, []byte(`"size":["尺码","尺寸","套餐","参考分类"]`)) {
|
||||
t.Fatal("built-in template must classify 参考分类 as size")
|
||||
}
|
||||
if !bytes.Contains(raw, []byte(`"orderSubmitAliases":["提交订单","提交訂單"]`)) {
|
||||
t.Fatal("built-in template must carry read-only collector text aliases")
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2ValidatesTransientSoldOutRecovery(t *testing.T) {
|
||||
@@ -80,9 +83,16 @@ func TestV2ValidatesTransientSoldOutRecovery(t *testing.T) {
|
||||
if err := Validate([]byte(valid)); err != nil {
|
||||
t.Fatalf("valid recovery rejected: %v", err)
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
for _, customized := range []string{
|
||||
strings.Replace(valid, `"exactText":"商品已售罄"`, `"exactText":"售罄"`, 1),
|
||||
strings.Replace(valid, `"fallbackTopText":"相似商品"`, `"fallbackTopText":"猜你喜欢"`, 1),
|
||||
} {
|
||||
if err := Validate([]byte(customized)); err != nil {
|
||||
t.Fatalf("custom recovery evidence rejected: %v", err)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
strings.Replace(valid, `"fallbackTopText":"相似商品"`, `"fallbackTopText":"收货地址"`, 1),
|
||||
strings.Replace(valid, `"pullDownCount":2`, `"pullDownCount":4`, 1),
|
||||
strings.Replace(valid, `"maxAttempts":1`, `"maxAttempts":2`, 1),
|
||||
} {
|
||||
@@ -96,6 +106,22 @@ func TestV2ValidatesTransientSoldOutRecovery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2CollectorTextAliasesAreReadOnlyAndStrict(t *testing.T) {
|
||||
raw := string(PDDProductDetailTemplate())
|
||||
if err := Validate([]byte(raw)); err != nil {
|
||||
t.Fatalf("payment/order evidence must be accepted: %v", err)
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
strings.Replace(raw, `"buyWords":["单独购买"`, `"buyWords":["收货地址"`, 1),
|
||||
strings.Replace(raw, `"titleAliases":["确认款式","確認款式"]`, `"titleAliases":["确认款式","确认款式"]`, 1),
|
||||
strings.Replace(raw, `"entryAliases":["商品评价"`, `"entryAliases":[] ,"ignored":["商品评价"`, 1),
|
||||
} {
|
||||
if err := Validate([]byte(invalid)); err == nil {
|
||||
t.Fatalf("invalid collector text aliases accepted: %s", invalid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestV2ValidatesReopenBrowserRecovery(t *testing.T) {
|
||||
base := validV2(`[]`)
|
||||
valid := strings.Replace(base, `"hooks"`, `"navigationRecovery":{"reopenBrowser":{"enabled":true,"maxAttempts":1,"settleMs":1000}},"hooks"`, 1)
|
||||
|
||||
@@ -18,6 +18,14 @@ var pddProductDetailTemplate = json.RawMessage(`{
|
||||
"collector":{
|
||||
"collectorId":"pddProductDetailV1","specEntryStrategy":"safeBottomSpecEntryV1","priceParser":"pddRmbPriceV1","priceGranularity":"color",
|
||||
"dimensionAliases":{"color":["颜色分类","颜色","款式","颜色款式","花色"],"size":["尺码","尺寸","套餐","参考分类"]},
|
||||
"textAliases":{
|
||||
"specPanel":{"titleAliases":["确认款式","確認款式"],"closeAliases":["关闭","關閉"],"confirmAliases":["确定","確定","确认","確認"],"quantityDecreaseAliases":["减少数量","減少數量"],"quantityIncreaseAliases":["增加数量","增加數量"],"paymentAreaAliases":["微信支付","先用后付","先用後付","支付方式"],"orderSubmitAliases":["提交订单","提交訂單"],"quickBuyAliases":["现在买","現在買"]},
|
||||
"dimension":{"colorSuffixes":["颜色","款式"],"sizeSuffixes":["尺码","尺寸"],"exactNames":["规格","型号","容量","类型","版本","口味"],"adaptiveAliases":["套餐"]},
|
||||
"selection":{"summaryPrefixes":["已选","请选择","已選","請選擇"],"selectedPrefixes":["已选","已選"],"specEntryPrefixes":["请选择","选择","已选"],"submitHintAliases":["选择","選擇","颜色","顏色","尺码","尺碼","规格","規格"]},
|
||||
"review":{"entryAliases":["商品评价","全部评价","评价详情","评论","晒单","问答"]},
|
||||
"soldOut":{"mainProductExactAliases":["进店","单独购买","发起拼单","立即购买"],"mainProductPrefixAliases":["商品评价"]},
|
||||
"purchase":{"buyWords":["单独购买","立即购买","发起拼单","去拼单","免拼购买","购买"]}
|
||||
},
|
||||
"timeoutsMs":{"page":30000,"specPanel":10000,"selection":2000,"price":2000,"overall":180000},
|
||||
"limits":{"goodsPageVerticalSwipes":3,"specHorizontalSwipes":12,"specVerticalSwipes":12,"stableEdgeReads":2,"stablePriceReads":2,"maxSkuCount":500}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user