From 573f5b8cf3f6f3486769ed1c418af49cca5ccfb1 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Thu, 24 Sep 2026 14:35:53 +0800 Subject: [PATCH 1/2] feat(syb): support 200/500 rows per page on SYB products list (#339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the syb-products page-size options to 20/50/100/200/500 with a new 100 default, cap the server-side sybimport.List page size at 500, chunk the per-page purchase-readiness preview into <=100-id requests, and add per-button selection limits (with disabled+tooltip) for AI 匹配, 创建采购, 创建采集, 图搜采集 and 匹配退货 so a larger page never silently exceeds a batch endpoint's cap. 创建采购's 100-item server cap is left untouched. Also caps returnmatch.BatchMatch at 500 ids (INVALID_REQUEST beyond that). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F --- server/app/goauto/returnmatch/handler.go | 4 ++ server/app/goauto/returnmatch/service.go | 13 +++++ server/app/goauto/returnmatch/service_test.go | 25 ++++++++++ server/app/goauto/sybimport/service.go | 4 +- server/app/goauto/sybimport/service_test.go | 30 ++++++++++++ web/src/views/goauto/syb-products/index.vue | 47 ++++++++++++++++--- 6 files changed, 114 insertions(+), 9 deletions(-) diff --git a/server/app/goauto/returnmatch/handler.go b/server/app/goauto/returnmatch/handler.go index eb16380..4f950b8 100644 --- a/server/app/goauto/returnmatch/handler.go +++ b/server/app/goauto/returnmatch/handler.go @@ -77,6 +77,10 @@ func (h Handler) BatchMatch(c *gin.Context) { } _, operator := operatorFromContext(c) resp, err := NewService(db).BatchMatch(c.Request.Context(), BatchMatchRequest{SYBProductIDs: body.SYBProductIDs, Operator: operator}) + if errors.Is(err, ErrTooManyItems) { + c.JSON(http.StatusBadRequest, gin.H{"code": "INVALID_REQUEST", "message": ErrTooManyItems.Error()}) + return + } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": "INTERNAL", "message": "服务端处理失败"}) return diff --git a/server/app/goauto/returnmatch/service.go b/server/app/goauto/returnmatch/service.go index f20adec..2b3f26c 100644 --- a/server/app/goauto/returnmatch/service.go +++ b/server/app/goauto/returnmatch/service.go @@ -68,10 +68,23 @@ const ( ReasonConflict = "conflict" ) +// maxBatchMatchItems caps a single「匹配退货」submission (#339: SYB list page +// size can now go up to 500/page, so the button's selection can exceed the +// previously-unbounded batch-match request size). +const maxBatchMatchItems = 500 + +// ErrTooManyItems is returned by BatchMatch when the caller submits more than +// maxBatchMatchItems SYB product ids; the handler turns this into an +// INVALID_REQUEST response instead of a 500. +var ErrTooManyItems = errors.New("sybProductIds 一次最多 500 条") + // BatchMatch implements issue #338's manual "匹配退货" trigger. It is only // ever called from the batch-match button (ticked rows) — no scheduler, no // yeeke-sync/SYB-import hook calls this (rule: 手动触发, 无定时任务). func (s *Service) BatchMatch(ctx context.Context, req BatchMatchRequest) (BatchMatchResponse, error) { + if len(req.SYBProductIDs) > maxBatchMatchItems { + return BatchMatchResponse{}, ErrTooManyItems + } resp, err := s.batchMatch(ctx, req) if len(req.SYBProductIDs) > 0 { // The batch record is written after the per-row transactions have diff --git a/server/app/goauto/returnmatch/service_test.go b/server/app/goauto/returnmatch/service_test.go index 00de4ca..0de5464 100644 --- a/server/app/goauto/returnmatch/service_test.go +++ b/server/app/goauto/returnmatch/service_test.go @@ -111,6 +111,31 @@ func TestBatchMatch_EndToEnd(t *testing.T) { } } +func TestBatchMatch_RejectsMoreThan500Items(t *testing.T) { + db := testDB(t) + s := NewService(db) + + ids := make([]uint64, maxBatchMatchItems+1) + for i := range ids { + ids[i] = uint64(i + 1) + } + resp, err := s.BatchMatch(context.Background(), BatchMatchRequest{SYBProductIDs: ids, Operator: "tester"}) + if !errors.Is(err, ErrTooManyItems) { + t.Fatalf("expected ErrTooManyItems, got %v", err) + } + if len(resp.Items) != 0 { + t.Fatalf("expected empty response on rejection, got %+v", resp) + } + + var batchCount int64 + if err := db.Model(&models.ReturnMatchBatch{}).Count(&batchCount).Error; err != nil { + t.Fatal(err) + } + if batchCount != 0 { + t.Fatalf("rejected oversized batch must not be recorded, got %d rows", batchCount) + } +} + func TestBatchMatch_ExpiredDeadlineNotMatched(t *testing.T) { db := testDB(t) s := NewService(db) diff --git a/server/app/goauto/sybimport/service.go b/server/app/goauto/sybimport/service.go index 2c9a1ff..480158c 100644 --- a/server/app/goauto/sybimport/service.go +++ b/server/app/goauto/sybimport/service.go @@ -65,8 +65,8 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp if request.PageSize < 1 { request.PageSize = 20 } - if request.PageSize > 100 { - request.PageSize = 100 + if request.PageSize > 500 { + request.PageSize = 500 } query := service.DB.WithContext(ctx).Model(&models.SYBProduct{}) if request.ShopName = strings.TrimSpace(request.ShopName); request.ShopName != "" { diff --git a/server/app/goauto/sybimport/service_test.go b/server/app/goauto/sybimport/service_test.go index 2eede97..ca7c6e9 100644 --- a/server/app/goauto/sybimport/service_test.go +++ b/server/app/goauto/sybimport/service_test.go @@ -93,6 +93,36 @@ func TestServiceListRejectsTooManyOrTooLongOrderCodes(t *testing.T) { } } +func TestServiceListCapsPageSizeAt500(t *testing.T) { + db := openTestDB(t) + order := realOrder() + for i := 0; i < 3; i++ { + detail := realDetailA() + detail.ID += uint64(i) + order.Code = fmt.Sprintf("260728TB95MJTQ-%d", i) + if _, err := sybimport.ApplyDetail(context.Background(), db, order, detail); err != nil { + t.Fatalf("apply detail %d: %v", i, err) + } + } + service := sybimport.NewService(db) + + overLimit, err := service.List(context.Background(), sybimport.ListRequest{Page: 1, PageSize: 600}) + if err != nil { + t.Fatalf("list with oversized page size: %v", err) + } + if overLimit.PageSize != 500 { + t.Fatalf("expected page size capped at 500, got %d", overLimit.PageSize) + } + + within, err := service.List(context.Background(), sybimport.ListRequest{Page: 1, PageSize: 500}) + if err != nil { + t.Fatalf("list at exactly 500: %v", err) + } + if within.PageSize != 500 { + t.Fatalf("expected page size of exactly 500 to pass through unchanged, got %d", within.PageSize) + } +} + func TestServiceListRejectsInvalidParseStatus(t *testing.T) { db := openTestDB(t) service := sybimport.NewService(db) diff --git a/web/src/views/goauto/syb-products/index.vue b/web/src/views/goauto/syb-products/index.vue index 4230ca5..f7a1d68 100644 --- a/web/src/views/goauto/syb-products/index.vue +++ b/web/src/views/goauto/syb-products/index.vue @@ -12,7 +12,7 @@ - 查询重置AI 匹配{{ aiMatchCandidates.length }}图搜采集{{ imageSearchRows.length }}创建采集{{ collectionCandidates.length }}创建采购{{ purchaseCandidates.length }}匹配退货{{ returnMatchCandidateIds.length }} + 查询重置AI 匹配{{ aiMatchCandidates.length }}图搜采集{{ imageSearchRows.length }}创建采集{{ collectionCandidates.length }}创建采购{{ purchaseCandidates.length }}匹配退货{{ returnMatchCandidateIds.length }} @@ -75,7 +75,7 @@ - + @@ -333,6 +333,12 @@ import PddProductDetailDrawer from '../pdd-products/PddProductDetailDrawer.vue' // 与服务端 image_search.go 的 imageSearchMaxBatchTasks 保持一致(单批去重后任务数上限,#280)。 const IMAGE_SEARCH_MAX_BATCH_TASKS = 50 +// #339: 各批量按钮的服务端上限,用于勾选数超限时禁用按钮并提示原因。 +// 创建采购 / AI 匹配:purchase/batch.go maxBatchPurchaseItems;创建采集(去重后 PDD 商品数): +// task/admin_service.go BatchCreate 的 100 上限;匹配退货:returnmatch/service.go maxBatchMatchItems。 +const PURCHASE_BATCH_MAX = 100 +const COLLECTION_BATCH_MAX = 100 +const RETURN_MATCH_BATCH_MAX = 500 export default { name: 'GoAutoSybProducts', @@ -353,7 +359,7 @@ export default { imageSearchBatch: this.emptyImageSearchBatch(), collectionBatchData: { ruleId: null, deviceId: null }, collectionBatchRules: { ruleId: [{ required: true, message: '请选择采集规则', trigger: 'change' }] }, - query: { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' }, + query: { page: 1, pageSize: 100, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' }, returnMatchByProductId: {}, returnMatchLoading: false, returnMatchBatchLoading: false, @@ -372,20 +378,34 @@ export default { processStageOptions() { return [{ value: 'manual_action', label: '待人工处理' }, { value: 'pdd_unlinked', label: '未关联 PDD' }, { value: 'pdd_pending', label: 'PDD 待采集' }, { value: 'pdd_collecting', label: 'PDD 采集中' }, { value: 'pdd_collection_failed', label: 'PDD 采集失败' }, { value: 'color_mapping', label: '规格待匹配' }, { value: 'purchase_ready', label: '可创建采购' }, { value: 'task_created', label: '已创建任务' }, { value: 'purchase_succeeded', label: '采购成功' }, { value: 'order_review', label: '待人工核对' }, { value: 'return_pending', label: '退货待确认' }, { value: 'return_used', label: '已用退货' }] }, returnMatchCandidateIds() { return this.selectedProducts.filter(row => this.isReturnMatchCandidate(row)).map(row => row.id) }, aiMatchCandidates() { return this.selectedProducts.filter(row => this.purchaseReady(row).aiMatchEligible === true) }, + aiMatchButtonOverLimit() { return this.aiMatchCandidates.length > PURCHASE_BATCH_MAX }, aiMatchButtonReason() { if (this.purchaseReadinessLoading) return '正在检查 AI 匹配资格' if (!this.selectedProducts.length) return '请先勾选当前页中可处理的明细' + if (this.aiMatchButtonOverLimit) return `一次最多 ${PURCHASE_BATCH_MAX} 条` if (this.aiMatchCandidates.length) return '' const reasons = [...new Set(this.selectedProducts.map(row => this.purchaseReady(row).aiMatchDisabledReason).filter(Boolean))] return reasons[0] || '所选明细不满足 AI 匹配前提' }, purchaseCandidates() { return this.selectedProducts.filter(row => this.isPurchaseCandidate(row)) }, + // #339: 创建采购上限保持 100(服务端 maxBatchPurchaseItems 不放宽),超出勾选数时禁用并提示。 + purchaseButtonOverLimit() { return this.purchaseCandidates.length > PURCHASE_BATCH_MAX }, + purchaseButtonReason() { return this.purchaseButtonOverLimit ? `一次最多 ${PURCHASE_BATCH_MAX} 条` : '' }, collectionCandidateRows() { return this.selectedProducts.filter(row => this.isCollectionCandidate(row)) }, collectionCandidates() { return [...new Set(this.collectionCandidateRows.map(row => this.purchaseReady(row).pddProductId))] }, + // #339: 创建采集按去重后的 PDD 商品数计数,与服务端 task.BatchCreate 的 100 上限口径一致。 + collectionButtonOverLimit() { return this.collectionCandidates.length > COLLECTION_BATCH_MAX }, + collectionButtonReason() { return this.collectionButtonOverLimit ? `一次最多 ${COLLECTION_BATCH_MAX} 个 PDD 商品` : '' }, imageSearchRows() { return this.selectedProducts.filter(row => row.shopeeProductId && row.imageUrl) }, // 服务端按去重后的蝦皮商品数创建任务(见 image_search.go 的 imageSearchMaxBatchTasks), // 这里用相同口径预估,避免提交后才发现超限。 imageSearchMaxBatchTasks() { return IMAGE_SEARCH_MAX_BATCH_TASKS }, + imageSearchButtonTaskCount() { return new Set(this.imageSearchRows.map(row => row.shopeeProductId)).size }, + imageSearchButtonOverLimit() { return this.imageSearchButtonTaskCount > IMAGE_SEARCH_MAX_BATCH_TASKS }, + imageSearchButtonReason() { return this.imageSearchButtonOverLimit ? `一次最多 ${IMAGE_SEARCH_MAX_BATCH_TASKS} 个蝦皮商品` : '' }, + // #339: 匹配退货服务端上限(returnmatch.maxBatchMatchItems),超出直接勾选数即禁用。 + returnMatchButtonOverLimit() { return this.returnMatchCandidateIds.length > RETURN_MATCH_BATCH_MAX }, + returnMatchButtonReason() { return this.returnMatchButtonOverLimit ? `一次最多 ${RETURN_MATCH_BATCH_MAX} 条` : '' }, imageSearchBatchTaskCount() { return new Set(this.imageSearchBatch.rows.map(row => row.shopeeProductId)).size }, imageSearchBatchOverLimit() { return this.imageSearchBatchTaskCount > IMAGE_SEARCH_MAX_BATCH_TASKS }, imageSearchBatchDurationText() { @@ -432,7 +452,7 @@ export default { } }, search() { this.query.page = 1; this.load() }, - reset() { this.query = { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' }; this.load() }, + reset() { this.query = { page: 1, pageSize: 100, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' }; this.load() }, normalizeShopName(value) { return String(value || '').normalize('NFKC').trim().toLocaleLowerCase() }, async ensureShopOptions() { if (this.shopOptionsLoaded) return @@ -477,16 +497,29 @@ export default { purchasePriceText(item) { if (item.minUnitPriceCent === undefined || item.maxUnitPriceCent === undefined) return ''; return `允许单价 ¥${(item.minUnitPriceCent / 100).toFixed(2)}~¥${(item.maxUnitPriceCent / 100).toFixed(2)}` }, processMeta(stage) { return { manual_action: { label: '待人工处理', type: 'warning' }, pdd_unlinked: { label: '未关联 PDD', type: 'info' }, pdd_pending: { label: 'PDD 待采集', type: 'info' }, pdd_collecting: { label: 'PDD 采集中', type: 'primary' }, pdd_collection_failed: { label: 'PDD 采集失败', type: 'danger' }, color_mapping: { label: '规格待匹配', type: 'warning' }, purchase_ready: { label: '可创建采购', type: 'success' }, task_created: { label: '已创建任务', type: 'primary' }, purchase_succeeded: { label: '采购成功', type: 'success' }, order_review: { label: '待人工核对', type: 'danger' }, return_pending: { label: '退货待确认', type: 'warning' }, return_used: { label: '已用退货', type: 'info' }}[stage] || { label: '待人工处理', type: 'warning' } }, purchaseActionLabel(item) { return { open_pdd_link: '去关联', open_mapping: '去匹配', open_shopee: '查看蝦皮商品', open_pdd: '查看 PDD 商品', open_task: '查看任务', reparse: '查看并处理', select_device: '重新选择设备', refresh: '刷新' }[item.nextAction] || '' }, + // #339: the page can now show up to 500 rows, but the preview endpoint + // (previewPurchaseTasks -> purchase/batch.go maxBatchPurchaseItems) still + // caps a single request at 100 ids, so this splits the current page into + // sequential chunks of <=100 and merges their results. Any chunk failing + // falls back to the same page-wide failure state the single-request path + // used before, so rows never end up in a partially-updated mix of real + // and failed readiness. async loadPurchaseReadiness(ids, requestOptions = {}, generation = this.loadGeneration, selectedIDsOverride = null) { if (generation !== this.loadGeneration) return const selectedIDs = selectedIDsOverride || new Set(this.selectedProducts.map(row => row.id)) this.purchaseReadiness = {} if (!this.canPurchase || !ids.length) { this.purchaseReadinessLoading = false; return } this.purchaseReadinessLoading = true + const chunkSize = 100 + const merged = {} try { - const r = await previewPurchaseTasks({ sybProductIds: ids }, { ...requestOptions, suppressErrorMessage: true }) - if (generation !== this.loadGeneration) return - this.purchaseReadiness = Object.fromEntries(r.data.items.map(item => [item.sybProductId, item])) + for (let start = 0; start < ids.length; start += chunkSize) { + const chunk = ids.slice(start, start + chunkSize) + const r = await previewPurchaseTasks({ sybProductIds: chunk }, { ...requestOptions, suppressErrorMessage: true }) + if (generation !== this.loadGeneration) return + for (const item of r.data.items) merged[item.sybProductId] = item + } + this.purchaseReadiness = merged await this.$nextTick() const valid = this.products.filter(row => selectedIDs.has(row.id) && this.isSelectableCandidate(row)) this.$refs.productTable?.clearSelection() From 977ac235e238815ab9cb8eabfe7136158e7526dc Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Sun, 27 Sep 2026 16:59:22 +0800 Subject: [PATCH 2/2] feat(syb): filter products by created time (#342) --- server/app/goauto/sybimport/handler.go | 2 +- server/app/goauto/sybimport/service.go | 50 +++++++++++++++++++++ server/app/goauto/sybimport/service_test.go | 41 +++++++++++++++++ web/src/views/goauto/syb-products/index.vue | 8 ++-- 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/server/app/goauto/sybimport/handler.go b/server/app/goauto/sybimport/handler.go index 7d0b823..2a8f417 100644 --- a/server/app/goauto/sybimport/handler.go +++ b/server/app/goauto/sybimport/handler.go @@ -47,7 +47,7 @@ func (handler Handler) List(c *gin.Context) { return } response, err := service.List(c.Request.Context(), ListRequest{ - Page: page, PageSize: pageSize, ShopName: c.Query("shopName"), OrderCodes: []string{c.Query("orderCodes")}, ParseStatus: strings.TrimSpace(c.Query("parseStatus")), ProcessStage: strings.TrimSpace(c.Query("processStage")), + Page: page, PageSize: pageSize, ShopName: c.Query("shopName"), OrderCodes: []string{c.Query("orderCodes")}, ParseStatus: strings.TrimSpace(c.Query("parseStatus")), ProcessStage: strings.TrimSpace(c.Query("processStage")), CreatedFrom: strings.TrimSpace(c.Query("createdFrom")), CreatedTo: strings.TrimSpace(c.Query("createdTo")), }) if err != nil { writeError(c, err) diff --git a/server/app/goauto/sybimport/service.go b/server/app/goauto/sybimport/service.go index 480158c..3fe2e00 100644 --- a/server/app/goauto/sybimport/service.go +++ b/server/app/goauto/sybimport/service.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "time" "go-admin/app/goauto/models" "go-admin/app/goauto/purchase" @@ -44,6 +45,8 @@ type ListRequest struct { OrderCodes []string ParseStatus string ProcessStage string + CreatedFrom string + CreatedTo string } type ListResponse struct { @@ -69,6 +72,16 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp request.PageSize = 500 } query := service.DB.WithContext(ctx).Model(&models.SYBProduct{}) + createdFrom, createdTo, err := createdAtRange(request.CreatedFrom, request.CreatedTo) + if err != nil { + return ListResponse{}, err + } + if createdFrom != nil { + query = query.Where("created_at >= ?", *createdFrom) + } + if createdTo != nil { + query = query.Where("created_at < ?", *createdTo) + } if request.ShopName = strings.TrimSpace(request.ShopName); request.ShopName != "" { if len([]rune(request.ShopName)) > 255 { return ListResponse{}, invalidRequest("店铺名称不能超过 255 个字符") @@ -142,6 +155,43 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp return ListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil } +// createdAtRange turns inclusive YYYY-MM-DD bounds into a half-open time +// range. The bounds are interpreted in the server's local timezone, matching +// the timestamps written by GORM for this service. +func createdAtRange(from, to string) (*time.Time, *time.Time, error) { + from = strings.TrimSpace(from) + to = strings.TrimSpace(to) + if from == "" && to == "" { + return nil, nil, nil + } + parse := func(value, label string) (*time.Time, error) { + if value == "" { + return nil, nil + } + parsed, err := time.ParseInLocation("2006-01-02", value, time.Local) + if err != nil { + return nil, invalidRequest(label + " 必须是 YYYY-MM-DD") + } + return &parsed, nil + } + start, err := parse(from, "createdFrom") + if err != nil { + return nil, nil, err + } + endDay, err := parse(to, "createdTo") + if err != nil { + return nil, nil, err + } + if start != nil && endDay != nil && start.After(*endDay) { + return nil, nil, invalidRequest("createdFrom 不能晚于 createdTo") + } + if endDay != nil { + end := endDay.AddDate(0, 0, 1) + endDay = &end + } + return start, endDay, nil +} + func normalizeOrderCodes(raw []string) ([]string, error) { seen := make(map[string]bool, len(raw)) result := make([]string, 0, len(raw)) diff --git a/server/app/goauto/sybimport/service_test.go b/server/app/goauto/sybimport/service_test.go index ca7c6e9..e104afc 100644 --- a/server/app/goauto/sybimport/service_test.go +++ b/server/app/goauto/sybimport/service_test.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" "testing" + "time" "go-admin/app/goauto/models" "go-admin/app/goauto/sybimport" @@ -123,6 +124,46 @@ func TestServiceListCapsPageSizeAt500(t *testing.T) { } } +func TestServiceListFiltersByCreatedDateInclusive(t *testing.T) { + db := openTestDB(t) + order := realOrder() + for i, day := range []string{"2026-09-01", "2026-09-02", "2026-09-03"} { + rowOrder := order + rowOrder.Code = fmt.Sprintf("CREATED-%d", i) + rowOrder.StockID += uint64(i) + detail := realDetailA() + detail.ID += uint64(i) + result, err := sybimport.ApplyDetail(context.Background(), db, rowOrder, detail) + if err != nil { + t.Fatal(err) + } + created, err := time.ParseInLocation("2006-01-02", day, time.Local) + if err != nil { + t.Fatal(err) + } + if err := db.Model(&models.SYBProduct{}).Where("id = ?", result.SYBProduct.ID).Update("created_at", created).Error; err != nil { + t.Fatal(err) + } + } + service := sybimport.NewService(db) + + between, err := service.List(context.Background(), sybimport.ListRequest{CreatedFrom: "2026-09-01", CreatedTo: "2026-09-02"}) + if err != nil || between.Total != 2 { + t.Fatalf("inclusive created date range should return two rows, total=%d err=%v", between.Total, err) + } + fromOnly, err := service.List(context.Background(), sybimport.ListRequest{CreatedFrom: "2026-09-03"}) + if err != nil || fromOnly.Total != 1 { + t.Fatalf("created-from filter should return one row, total=%d err=%v", fromOnly.Total, err) + } + toOnly, err := service.List(context.Background(), sybimport.ListRequest{CreatedTo: "2026-09-01"}) + if err != nil || toOnly.Total != 1 { + t.Fatalf("created-to filter should return one row, total=%d err=%v", toOnly.Total, err) + } + if _, err := service.List(context.Background(), sybimport.ListRequest{CreatedFrom: "2026-09-04", CreatedTo: "2026-09-01"}); serviceErrCode(t, err) != sybimport.CodeInvalidRequest { + t.Fatalf("reversed created date range should be rejected: %v", err) + } +} + func TestServiceListRejectsInvalidParseStatus(t *testing.T) { db := openTestDB(t) service := sybimport.NewService(db) diff --git a/web/src/views/goauto/syb-products/index.vue b/web/src/views/goauto/syb-products/index.vue index f7a1d68..91fff83 100644 --- a/web/src/views/goauto/syb-products/index.vue +++ b/web/src/views/goauto/syb-products/index.vue @@ -12,6 +12,7 @@ + 查询重置AI 匹配{{ aiMatchCandidates.length }}图搜采集{{ imageSearchRows.length }}创建采集{{ collectionCandidates.length }}创建采购{{ purchaseCandidates.length }}匹配退货{{ returnMatchCandidateIds.length }} @@ -359,7 +360,7 @@ export default { imageSearchBatch: this.emptyImageSearchBatch(), collectionBatchData: { ruleId: null, deviceId: null }, collectionBatchRules: { ruleId: [{ required: true, message: '请选择采集规则', trigger: 'change' }] }, - query: { page: 1, pageSize: 100, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' }, + query: { page: 1, pageSize: 200, shopName: '', orderCodesText: '', parseStatus: '', processStage: '', createdAtRange: [] }, returnMatchByProductId: {}, returnMatchLoading: false, returnMatchBatchLoading: false, @@ -432,7 +433,8 @@ export default { this.$refs.productTable?.clearSelection() const requestOptions = allowNetworkRetry ? { suppressNetworkError: true } : {} try { - const r = await listSybProducts({ page: this.query.page, pageSize: this.query.pageSize, shopName: this.query.shopName.trim(), orderCodes: orderCodes.join(','), parseStatus: this.query.parseStatus, processStage: this.query.processStage }, requestOptions) + const [createdFrom = '', createdTo = ''] = this.query.createdAtRange || [] + const r = await listSybProducts({ page: this.query.page, pageSize: this.query.pageSize, shopName: this.query.shopName.trim(), orderCodes: orderCodes.join(','), parseStatus: this.query.parseStatus, processStage: this.query.processStage, createdFrom, createdTo }, requestOptions) if (generation !== this.loadGeneration) return this.products = r.data.items this.total = r.data.total @@ -452,7 +454,7 @@ export default { } }, search() { this.query.page = 1; this.load() }, - reset() { this.query = { page: 1, pageSize: 100, shopName: '', orderCodesText: '', parseStatus: '', processStage: '' }; this.load() }, + reset() { this.query = { page: 1, pageSize: 200, shopName: '', orderCodesText: '', parseStatus: '', processStage: '', createdAtRange: [] }; this.load() }, normalizeShopName(value) { return String(value || '').normalize('NFKC').trim().toLocaleLowerCase() }, async ensureShopOptions() { if (this.shopOptionsLoaded) return