From 0fccbc99c1d675b64d52ae337737498f8f8c709d Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 24 Aug 2026 15:10:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(#74):=20=E4=BC=98=E5=8C=96=20SYB=20?= =?UTF-8?q?=E5=95=86=E5=93=81=E7=AD=9B=E9=80=89=E5=92=8C=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/02-architecture-and-code-map.md | 6 +-- docs/03-business-rules-and-glossary.md | 5 +- server/app/goauto/sybimport/handler.go | 2 +- server/app/goauto/sybimport/service.go | 40 +++++++++++++-- server/app/goauto/sybimport/service_test.go | 56 +++++++++++++++++++-- web/src/views/goauto/syb-products/index.vue | 26 ++++++---- web/tests/e2e/syb-network-retry.spec.ts | 4 +- web/tests/e2e/syb-product-layout.spec.ts | 24 +++++++-- 8 files changed, 134 insertions(+), 29 deletions(-) diff --git a/docs/02-architecture-and-code-map.md b/docs/02-architecture-and-code-map.md index cafd0a9..db7a30a 100644 --- a/docs/02-architecture-and-code-map.md +++ b/docs/02-architecture-and-code-map.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Architecture-and-Code-Map wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Architecture-and-Code-Map.- -wiki_revision: 9bb0da18b68d368b37c060af9867983ace6b49f6 -synchronized_at: 2026-08-24T03:42:12Z +wiki_revision: 5f88de723d9ca9124383cd4abcfa5c27ddcd6d48 +synchronized_at: 2026-08-24T07:03:25Z # 架构与代码地图 @@ -130,7 +130,7 @@ Android Portal/Agent | Admin AI 规格匹配设置 | `web/src/views/goauto/ai-matching-settings/`、`web/src/api/goauto/ai-matching-settings.js`;管理员可查看、维护和测试 Provider(包括内部明文 API Key),采购员只可查看启用状态 | | Admin 失败采购任务批量重试 | `POST /api/admin/v1/purchase-tasks/batch-retry`;服务端 `server/app/goauto/purchase/retry.go` 负责资格判定、逐项幂等创建与部分成功结果,Admin 页面只允许选择服务端标记可重试的行;不修改 Android Agent | | SYB 店铺管理页面与接口封装 | `web/src/views/goauto/syb-shops/`、`web/src/api/goauto/syb-shops.js`;确认原型快照 `prototypes/49/v1/index.html` | -| SYB 异步导入、当前页采购选择/确认/逐条结果与同步记录页面 | `web/src/views/goauto/syb-products/`、`web/src/views/goauto/syb-sync-runs/`、`web/src/api/goauto/syb-products.js`、`web/src/api/goauto/purchase-tasks.js`;确认原型见 #44 设计证据,导入原型快照为 `prototypes/50/v2/index.html` | +| SYB 异步导入、商品筛选、当前页采购选择/确认/逐条结果与同步记录页面 | `web/src/views/goauto/syb-products/`、`web/src/views/goauto/syb-sync-runs/`、`web/src/api/goauto/syb-products.js`、`web/src/api/goauto/purchase-tasks.js`;商品列表按店铺名称包含匹配,并支持最多 100 个多行订单号精确筛选;确认原型见 #44 设计证据,导入原型快照为 `prototypes/50/v2/index.html` | | Android Agent 基线 | `android/app/src/main/java/cn/ilapage/goauto/agent/` | | Android 无障碍规则执行 | `android/app/src/main/java/cn/ilapage/goauto/agent/automation/` | | PDD 安全规格入口与评论页误触恢复 | `automation/PddProductDetailCollector.kt`:明确选择语义、底部购买文字、评论上下文排除、一次返回重试和结构化错误;不保存控件树或截图 | diff --git a/docs/03-business-rules-and-glossary.md b/docs/03-business-rules-and-glossary.md index ee1fbcc..0402d8d 100644 --- a/docs/03-business-rules-and-glossary.md +++ b/docs/03-business-rules-and-glossary.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Business-Rules-and-Glossary wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Business-Rules-and-Glossary.- -wiki_revision: 14e4596838288a4685ffac6b55b17fcecba40326 -synchronized_at: 2026-08-24T03:42:20Z +wiki_revision: 5adb7b0f05c4562e23fe92fe7ff89625d697c2d4 +synchronized_at: 2026-08-24T07:03:44Z # 业务规则与术语 @@ -46,6 +46,7 @@ synchronized_at: 2026-08-24T03:42:20Z - 关联虾皮商品档案:命中存活记录直接关联;命中软删除记录则复活并保留原有人工映射;均不存在时创建最小档案。命中既有档案时只补写为空的参考图与售价,不覆盖人工修改过的标题、店铺和映射。 - 重新解析只读取已保存的原始 JSON,不请求 SYB 接口,永不改写 `RawJSON`;标记 `manuallyConfirmed` 的明细默认跳过重新解析,除非显式指定强制覆盖。 - 人工修正的颜色/尺码视为与解析成功同等可信,同样会合并进虾皮商品档案;标记为已人工确认。 +- Admin 商品列表的订单号筛选每行一个、去空去重后精确匹配,最多 100 个且单项最长 64 字符;店铺名称使用包含匹配。店铺、订单号与解析状态之间使用 AND,不再用该筛选框搜索蝦皮商品 ID 或商品标题。 - SYB 商品数据本身不保存 PDD 订单号、快递单号、支付或有效订单状态;Admin 的采购任务创建入口位于 SYB 商品列表,任务事实仍写入独立采购域。 ## SYB 店铺过滤 diff --git a/server/app/goauto/sybimport/handler.go b/server/app/goauto/sybimport/handler.go index bd8fde4..e1cb72e 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, Keyword: c.Query("keyword"), ParseStatus: strings.TrimSpace(c.Query("parseStatus")), + Page: page, PageSize: pageSize, ShopName: c.Query("shopName"), OrderCodes: []string{c.Query("orderCodes")}, ParseStatus: strings.TrimSpace(c.Query("parseStatus")), }) if err != nil { writeError(c, err) diff --git a/server/app/goauto/sybimport/service.go b/server/app/goauto/sybimport/service.go index 7e7c0f7..c5c543f 100644 --- a/server/app/goauto/sybimport/service.go +++ b/server/app/goauto/sybimport/service.go @@ -39,7 +39,8 @@ func NewService(db *gorm.DB) *Service { return &Service{DB: db} } type ListRequest struct { Page, PageSize int - Keyword string + ShopName string + OrderCodes []string ParseStatus string } @@ -65,9 +66,18 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp request.PageSize = 100 } query := service.DB.WithContext(ctx).Model(&models.SYBProduct{}) - if request.Keyword = strings.TrimSpace(request.Keyword); request.Keyword != "" { - like := "%" + request.Keyword + "%" - query = query.Where("order_code LIKE ? OR shopee_item_id LIKE ? OR product_title LIKE ?", like, like, like) + if request.ShopName = strings.TrimSpace(request.ShopName); request.ShopName != "" { + if len([]rune(request.ShopName)) > 255 { + return ListResponse{}, invalidRequest("店铺名称不能超过 255 个字符") + } + query = query.Where("shop_name LIKE ?", "%"+request.ShopName+"%") + } + orderCodes, err := normalizeOrderCodes(request.OrderCodes) + if err != nil { + return ListResponse{}, err + } + if len(orderCodes) > 0 { + query = query.Where("order_code IN ?", orderCodes) } if request.ParseStatus != "" { if request.ParseStatus != models.SYBParseStatusSuccess && request.ParseStatus != models.SYBParseStatusUncertain && request.ParseStatus != models.SYBParseStatusFailed { @@ -86,6 +96,28 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp return ListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil } +func normalizeOrderCodes(raw []string) ([]string, error) { + seen := make(map[string]bool, len(raw)) + result := make([]string, 0, len(raw)) + for _, chunk := range raw { + for _, value := range strings.FieldsFunc(chunk, func(r rune) bool { return r == ',' || r == '\r' || r == '\n' }) { + value = strings.TrimSpace(value) + if value == "" || seen[value] { + continue + } + if len([]rune(value)) > 64 { + return nil, invalidRequest("订单号不能超过 64 个字符") + } + seen[value] = true + result = append(result, value) + if len(result) > 100 { + return nil, invalidRequest("一次最多查询 100 个订单号") + } + } + } + return result, nil +} + // DetailView exposes RawJSON, which the model deliberately keeps json:"-" so // List (and any other place a bare models.SYBProduct gets serialized) never // leaks the full raw payload by accident. This was caught before it shipped: diff --git a/server/app/goauto/sybimport/service_test.go b/server/app/goauto/sybimport/service_test.go index 5e6394d..87d3974 100644 --- a/server/app/goauto/sybimport/service_test.go +++ b/server/app/goauto/sybimport/service_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "strings" "testing" @@ -20,7 +21,7 @@ func serviceErrCode(t *testing.T, err error) string { return target.Code } -func TestServiceListFiltersByKeywordAndParseStatus(t *testing.T) { +func TestServiceListFiltersByOrderCodesShopAndParseStatus(t *testing.T) { db := openTestDB(t) if _, err := sybimport.ApplyDetail(context.Background(), db, realOrder(), realDetailA()); err != nil { t.Fatalf("apply a: %v", err) @@ -28,14 +29,39 @@ func TestServiceListFiltersByKeywordAndParseStatus(t *testing.T) { if _, err := sybimport.ApplyDetail(context.Background(), db, realOrder(), realDetailB()); err != nil { t.Fatalf("apply b: %v", err) } + otherOrder := realOrder() + otherOrder.Code = "260729OTHER" + otherOrder.StockID++ + otherOrder.ShopName = "其他店铺" + otherDetail := realDetailA() + otherDetail.ID++ + if _, err := sybimport.ApplyDetail(context.Background(), db, otherOrder, otherDetail); err != nil { + t.Fatalf("apply other order: %v", err) + } service := sybimport.NewService(db) - byKeyword, err := service.List(context.Background(), sybimport.ListRequest{Keyword: "260728TB95MJTQ"}) + byOrders, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: []string{" 260728TB95MJTQ\n", "260729OTHER", "260728TB95MJTQ"}}) if err != nil { - t.Fatalf("list by keyword: %v", err) + t.Fatalf("list by order codes: %v", err) } - if byKeyword.Total != 2 { - t.Fatalf("expected 2 rows for the order code, got %d", byKeyword.Total) + if byOrders.Total != 3 { + t.Fatalf("expected all 3 rows for two exact order codes, got %d", byOrders.Total) + } + + combined, err := service.List(context.Background(), sybimport.ListRequest{ShopName: " 韓舍 ", OrderCodes: []string{"260728TB95MJTQ", "260729OTHER"}, ParseStatus: models.SYBParseStatusSuccess}) + if err != nil { + t.Fatalf("list by combined filters: %v", err) + } + if combined.Total != 1 || combined.Items[0].OrderCode != "260728TB95MJTQ" { + t.Fatalf("shop, order and status filters must use AND: %+v", combined) + } + + partialOrder, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: []string{"260728"}}) + if err != nil { + t.Fatalf("list by partial order: %v", err) + } + if partialOrder.Total != 0 { + t.Fatalf("order codes must use exact matching, got %d rows", partialOrder.Total) } byStatus, err := service.List(context.Background(), sybimport.ListRequest{ParseStatus: models.SYBParseStatusUncertain}) @@ -47,6 +73,26 @@ func TestServiceListFiltersByKeywordAndParseStatus(t *testing.T) { } } +func TestServiceListRejectsTooManyOrTooLongOrderCodes(t *testing.T) { + service := sybimport.NewService(openTestDB(t)) + tooMany := make([]string, 101) + for i := range tooMany { + tooMany[i] = fmt.Sprintf("ORDER-%03d", i) + } + if code := serviceErrCode(t, func() error { + _, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: tooMany}) + return err + }()); code != sybimport.CodeInvalidRequest { + t.Fatalf("expected too-many rejection, got %s", code) + } + if code := serviceErrCode(t, func() error { + _, err := service.List(context.Background(), sybimport.ListRequest{OrderCodes: []string{strings.Repeat("A", 65)}}) + return err + }()); code != sybimport.CodeInvalidRequest { + t.Fatalf("expected too-long rejection, got %s", code) + } +} + 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 3dab71b..cbb10ba 100644 --- a/web/src/views/goauto/syb-products/index.vue +++ b/web/src/views/goauto/syb-products/index.vue @@ -6,7 +6,8 @@ 导入 同步记录 已选择 {{ selectedProducts.length }} 条 - + + 查询重置创建采购 @@ -24,15 +25,14 @@ - + - - - + +

本页不展示采购任务、PDD 订单号、快递单号和支付状态。

@@ -157,7 +157,7 @@ export default { purchaseDevices: [], purchaseDialog: { open: false, loading: false, saving: false, ids: [], deviceId: null, items: [], eligibleCount: 0, skippedCount: 0 }, purchaseResult: { open: false, items: [], createdCount: 0, failedCount: 0 }, - query: { page: 1, pageSize: 20, keyword: '', parseStatus: '' }, + query: { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '' }, importDialog: { open: false, saving: false, range: null, spanDays: 0 }, activeRun: null, syncPollTimer: null, detail: { open: false, loading: false, item: null }, @@ -185,13 +185,15 @@ export default { beforeUnmount() { this.stopSyncPolling() }, methods: { async load(allowNetworkRetry = true) { + const orderCodes = this.normalizedOrderCodes() + if (orderCodes === null) return const generation = ++this.loadGeneration this.loading = true this.selectedProducts = [] this.$refs.productTable?.clearSelection() const requestOptions = allowNetworkRetry ? { suppressNetworkError: true } : {} try { - const r = await listSybProducts(this.query, requestOptions) + const r = await listSybProducts({ page: this.query.page, pageSize: this.query.pageSize, shopName: this.query.shopName.trim(), orderCodes: orderCodes.join(','), parseStatus: this.query.parseStatus }, requestOptions) if (generation !== this.loadGeneration) return this.products = r.data.items this.total = r.data.total @@ -210,7 +212,13 @@ export default { } }, search() { this.query.page = 1; this.load() }, - reset() { this.query = { page: 1, pageSize: 20, keyword: '', parseStatus: '' }; this.load() }, + reset() { this.query = { page: 1, pageSize: 20, shopName: '', orderCodesText: '', parseStatus: '' }; this.load() }, + normalizedOrderCodes() { + const values = [...new Set(this.query.orderCodesText.split(/\r?\n/).map(value => value.trim()).filter(Boolean))] + if (values.length > 100) { ElMessage.warning('一次最多查询 100 个订单号'); return null } + if (values.some(value => [...value].length > 64)) { ElMessage.warning('订单号不能超过 64 个字符'); return null } + return values + }, handleSelectionChange(rows) { this.selectedProducts = rows }, rowSelectable(row) { return this.canPurchase && this.purchaseReady(row).eligible }, purchaseReady(row) { return this.purchaseReadiness[row.id] || { sybProductId: row.id, eligible: false, reason: this.purchaseReadinessLoading ? '正在检查' : '请刷新后重试' } }, @@ -379,7 +387,7 @@ export default { .search-form{display:flex;flex-wrap:wrap;align-items:center;column-gap:8px;padding:12px 16px 0;margin-bottom:16px;border:1px solid #e5e7eb;border-radius:8px;background:#f8fafc} .search-form :deep(.el-form-item){margin-right:0;margin-bottom:12px} .search-form :deep(.el-form-item__content){flex-wrap:nowrap} -.keyword-input{width:170px} +.shop-input{width:130px}.order-codes-input{width:210px}.unlinked-tag{margin-left:8px} .toolbar-actions :deep(.el-form-item__content){gap:8px} .toolbar-actions :deep(.el-button + .el-button){margin-left:0} .selection-count{display:inline-flex;align-items:center;min-height:32px;white-space:nowrap;color:#606266} diff --git a/web/tests/e2e/syb-network-retry.spec.ts b/web/tests/e2e/syb-network-retry.spec.ts index e135370..3233a4f 100644 --- a/web/tests/e2e/syb-network-retry.spec.ts +++ b/web/tests/e2e/syb-network-retry.spec.ts @@ -85,7 +85,7 @@ test('新的查询结果不会被旧采购预检覆盖', async({ page, context } if (url.pathname.startsWith('/src/api/')) return route.continue() if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}}) if (url.pathname.endsWith('/api/admin/v1/syb-products')) { - const searched = url.searchParams.get('keyword') === '新商品' + const searched = url.searchParams.get('orderCodes') === 'NEW-76' const item = searched ? { id: 76, orderCode: 'NEW-76', shopeeItemId: '10076', productTitle: '新商品', quantity: 1, parseStatus: 'success' } : { id: 75, orderCode: 'OLD-75', shopeeItemId: '10075', productTitle: '旧商品', quantity: 1, parseStatus: 'success' } @@ -104,7 +104,7 @@ test('新的查询结果不会被旧采购预检覆盖', async({ page, context } await page.goto('http://localhost:9527/#/syb-products/index') await expect(page.getByText('旧商品')).toBeVisible() - await page.getByPlaceholder('订单号、虾皮商品ID或商品标题').fill('新商品') + await page.getByPlaceholder('每行一个订单号,最多 100 个').fill('NEW-76') await page.getByRole('button', { name: '查询' }).click() await expect(page.getByText('新商品', { exact: true })).toBeVisible() await expect(page.getByText('新商品暂不可创建')).toBeVisible() diff --git a/web/tests/e2e/syb-product-layout.spec.ts b/web/tests/e2e/syb-product-layout.spec.ts index 7316dbb..2b97d13 100644 --- a/web/tests/e2e/syb-product-layout.spec.ts +++ b/web/tests/e2e/syb-product-layout.spec.ts @@ -1,13 +1,15 @@ import { expect, test } from '@playwright/test' test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context }) => { + await page.setViewportSize({ width: 1920, height: 1080 }) await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }]) + const listURLs: URL[] = [] await page.route('**/api/**', async route => { const url = new URL(route.request().url()) if (url.pathname.startsWith('/src/api/')) return route.continue() if (url.pathname.endsWith('/api/v1/getinfo')) return route.fulfill({ json: { code: 200, data: { roles: ['admin'], name: '管理员', avatar: '', introduction: '', permissions: [] }}}) - if (url.pathname.endsWith('/api/admin/v1/syb-products')) return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, orderCode: 'SYB001', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '用于验证加宽后的虾皮商品标题区域', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 2, unitPriceCent: 1000, parseStatus: 'success', manuallyConfirmed: false }], total: 1, page: 1, pageSize: 20 }}}) - if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 1, eligible: true, minUnitPriceCent: 200, maxUnitPriceCent: 1500 }], eligibleCount: 1, skippedCount: 0 }}}) + if (url.pathname.endsWith('/api/admin/v1/syb-products')) { listURLs.push(url); return route.fulfill({ json: { code: 200, data: { items: [{ id: 1, orderCode: 'SYB001', shopeeItemId: '26154802794', shopeeProductId: 9, productTitle: '用于验证加宽后的虾皮商品标题区域', shopName: '大碼臻選', targetColor: '黑色', targetSize: 'XL', quantity: 2, unitPriceCent: 1000, parseStatus: 'success', manuallyConfirmed: false }, { id: 2, orderCode: 'SYB002', shopeeItemId: '26154802795', productTitle: '尚未关联的商品', shopName: '大碼臻選', targetColor: '白色', targetSize: 'L', quantity: 1, unitPriceCent: 900, parseStatus: 'success', manuallyConfirmed: false }], total: 2, page: 1, pageSize: 20 }}}) } + if (url.pathname.endsWith('/api/admin/v1/purchase-tasks/batch-preview')) return route.fulfill({ json: { code: 200, data: { items: [{ sybProductId: 1, eligible: true, minUnitPriceCent: 200, maxUnitPriceCent: 1500 }, { sybProductId: 2, eligible: false, reason: '尚未关联蝦皮商品' }], eligibleCount: 1, skippedCount: 1 }}}) return route.fulfill({ json: { code: 200, data: [] }}) }) @@ -17,6 +19,9 @@ test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context await expect(page.getByRole('button', { name: '从 SYB 导入', exact: true })).toHaveCount(0) await expect(page.getByText('已选择 0 条', { exact: true })).toBeVisible() await expect(page.getByRole('button', { name: '重新解析', exact: true })).toHaveCount(0) + await expect(page.getByRole('columnheader', { name: '蝦皮关联' })).toHaveCount(0) + await expect(page.getByText('未关联', { exact: true }).last()).toBeVisible() + await expect(page.getByText('26154802795 ↗', { exact: true })).toHaveCount(0) const importButton = await page.getByRole('button', { name: '导入', exact: true }).boundingBox() const queryButton = await page.getByRole('button', { name: '查询', exact: true }).boundingBox() @@ -27,11 +32,24 @@ test('SYB 商品工具栏与列宽按紧凑布局显示', async({ page, context expect(Math.abs((reset?.y || 0) - (purchase?.y || 0))).toBeLessThan(2) expect(purchase?.x || 0).toBeGreaterThan((reset?.x || 0) + (reset?.width || 0)) await expect(page.getByRole('button', { name: '创建采购', exact: true })).toBeDisabled() + const shopInput = await page.getByPlaceholder('输入店铺名称').boundingBox() + const orderInput = await page.getByPlaceholder('每行一个订单号,最多 100 个').boundingBox() + expect(shopInput).not.toBeNull(); expect(orderInput).not.toBeNull() + expect(shopInput?.x || 0).toBeLessThan(orderInput?.x || 0) await page.locator('.el-table__body-wrapper tbody tr').first().locator('.el-checkbox').click() await expect(page.getByText('已选择 1 条', { exact: true })).toBeVisible() await expect(page.getByRole('button', { name: '创建采购', exact: true })).toBeEnabled() const widths = await page.locator('.el-table__header-wrapper colgroup col').evaluateAll(cols => cols.map(col => Number(col.getAttribute('width')))) - expect(widths.slice(0, 10)).toEqual([48, 76, 120, 421, 100, 128, 56, 110, 80, 80]) + expect(widths[9]).toBeGreaterThanOrEqual(210) + expect(widths[10]).toBe(132) + + await page.getByPlaceholder('输入店铺名称').fill('大碼') + await page.getByPlaceholder('每行一个订单号,最多 100 个').fill(' SYB001\n\nSYB002\nSYB001 ') + await page.getByPlaceholder('每行一个订单号,最多 100 个').press('Control+Enter') + await expect.poll(() => listURLs.length).toBeGreaterThan(1) + const latest = listURLs.at(-1) + expect(latest?.searchParams.get('shopName')).toBe('大碼') + expect(latest?.searchParams.get('orderCodes')).toBe('SYB001,SYB002') })