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