fix(#84): restore collection selection in locate mode
This commit is contained in:
@@ -100,6 +100,10 @@ type SaveResponse struct {
|
||||
Replayed bool `json:"replayed,omitempty"`
|
||||
}
|
||||
|
||||
type DetailResponse struct {
|
||||
Product ListProductView `json:"product"`
|
||||
}
|
||||
|
||||
type Service struct{ DB *gorm.DB }
|
||||
|
||||
func NewService(db *gorm.DB) *Service { return &Service{DB: db} }
|
||||
@@ -197,19 +201,27 @@ func (service *Service) Update(ctx context.Context, productID uint64, request Up
|
||||
if result.RowsAffected == 0 {
|
||||
return SaveResponse{}, &ServiceError{Code: CodeProductNotFound, Message: "PDD 商品不存在"}
|
||||
}
|
||||
return service.Detail(ctx, productID)
|
||||
var updated models.PDDProduct
|
||||
if err := db.First(&updated, productID).Error; err != nil {
|
||||
return SaveResponse{}, internalError(err)
|
||||
}
|
||||
view, err := makeView(updated)
|
||||
return SaveResponse{Product: view}, err
|
||||
}
|
||||
|
||||
func (service *Service) Detail(ctx context.Context, productID uint64) (SaveResponse, error) {
|
||||
func (service *Service) Detail(ctx context.Context, productID uint64) (DetailResponse, error) {
|
||||
var record models.PDDProduct
|
||||
if err := service.DB.WithContext(ctx).First(&record, productID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return SaveResponse{}, &ServiceError{Code: CodeProductNotFound, Message: "PDD 商品不存在"}
|
||||
return DetailResponse{}, &ServiceError{Code: CodeProductNotFound, Message: "PDD 商品不存在"}
|
||||
}
|
||||
return SaveResponse{}, internalError(err)
|
||||
return DetailResponse{}, internalError(err)
|
||||
}
|
||||
view, err := makeView(record)
|
||||
return SaveResponse{Product: view}, err
|
||||
items, err := service.makeCollectionViews(ctx, []models.PDDProduct{record})
|
||||
if err != nil {
|
||||
return DetailResponse{}, err
|
||||
}
|
||||
return DetailResponse{Product: items[0]}, nil
|
||||
}
|
||||
|
||||
func (service *Service) List(ctx context.Context, request ListRequest) (ListResponse, error) {
|
||||
@@ -244,6 +256,18 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp
|
||||
if err := query.Order("updated_at DESC, id DESC").Offset((request.Page - 1) * request.PageSize).Limit(request.PageSize).Find(&records).Error; err != nil {
|
||||
return ListResponse{}, internalError(err)
|
||||
}
|
||||
items, err := service.makeCollectionViews(ctx, records)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
}
|
||||
return ListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil
|
||||
}
|
||||
|
||||
func (service *Service) makeCollectionViews(ctx context.Context, records []models.PDDProduct) ([]ListProductView, error) {
|
||||
items := make([]ListProductView, 0, len(records))
|
||||
if len(records) == 0 {
|
||||
return items, nil
|
||||
}
|
||||
productIDs := make([]uint64, 0, len(records))
|
||||
for _, record := range records {
|
||||
productIDs = append(productIDs, record.ID)
|
||||
@@ -253,27 +277,24 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp
|
||||
TaskID uint64
|
||||
}
|
||||
activeTasks := map[uint64]uint64{}
|
||||
if len(productIDs) > 0 {
|
||||
var rows []activeTaskRow
|
||||
if err := service.DB.WithContext(ctx).Model(&models.CollectionTask{}).
|
||||
Select("pdd_product_id, MIN(id) AS task_id").
|
||||
Where("pdd_product_id IN ? AND status IN ?", productIDs, []string{models.TaskStatusPending, models.TaskStatusRunning}).
|
||||
Group("pdd_product_id").Scan(&rows).Error; err != nil {
|
||||
return ListResponse{}, internalError(err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
activeTasks[row.PDDProductID] = row.TaskID
|
||||
}
|
||||
var rows []activeTaskRow
|
||||
if err := service.DB.WithContext(ctx).Model(&models.CollectionTask{}).
|
||||
Select("pdd_product_id, MIN(id) AS task_id").
|
||||
Where("pdd_product_id IN ? AND status IN ?", productIDs, []string{models.TaskStatusPending, models.TaskStatusRunning}).
|
||||
Group("pdd_product_id").Scan(&rows).Error; err != nil {
|
||||
return nil, internalError(err)
|
||||
}
|
||||
for _, row := range rows {
|
||||
activeTasks[row.PDDProductID] = row.TaskID
|
||||
}
|
||||
var ruleCount int64
|
||||
if err := service.DB.WithContext(ctx).Model(&models.CollectionRule{}).Count(&ruleCount).Error; err != nil {
|
||||
return ListResponse{}, internalError(err)
|
||||
return nil, internalError(err)
|
||||
}
|
||||
items := make([]ListProductView, 0, len(records))
|
||||
for _, record := range records {
|
||||
view, err := makeView(record)
|
||||
if err != nil {
|
||||
return ListResponse{}, err
|
||||
return nil, err
|
||||
}
|
||||
item := ListProductView{ProductView: view, CollectionSelectable: true}
|
||||
if record.Status == "disabled" {
|
||||
@@ -289,7 +310,7 @@ func (service *Service) List(ctx context.Context, request ListRequest) (ListResp
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return ListResponse{Items: items, Total: total, Page: request.Page, PageSize: request.PageSize}, nil
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func validateCreateRequest(request SaveRequest) error {
|
||||
|
||||
@@ -190,3 +190,49 @@ func TestListDisablesCollectionWhenNoRuleExists(t *testing.T) {
|
||||
t.Fatalf("missing-rule product remained selectable: %+v", listed.Items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetailUsesSameCollectionEligibilityAsList(t *testing.T) {
|
||||
db := openProductDatabase(t)
|
||||
service := NewService(db)
|
||||
rule := models.CollectionRule{Name: "rule", ContentJSON: `{}`}
|
||||
if err := db.Create(&rule).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
products := []models.PDDProduct{
|
||||
{GoodsID: "555555", URL: "https://mobile.yangkeduo.com/goods.html?goods_id=555555", Status: "active"},
|
||||
{GoodsID: "666666", URL: "https://mobile.yangkeduo.com/goods.html?goods_id=666666", Status: "disabled"},
|
||||
{GoodsID: "777777", URL: "https://mobile.yangkeduo.com/goods.html?goods_id=777777", Status: "active"},
|
||||
}
|
||||
if err := db.Create(&products).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
active := models.CollectionTask{
|
||||
PDDProductID: products[2].ID, RuleID: rule.ID, Status: models.TaskStatusPending,
|
||||
URLSnapshot: products[2].URL, GoodsIDSnapshot: products[2].GoodsID, RuleSnapshot: rule.ContentJSON,
|
||||
}
|
||||
if err := db.Create(&active).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listed, err := service.List(context.Background(), ListRequest{Page: 1, PageSize: 20})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
byID := map[uint64]ListProductView{}
|
||||
for _, item := range listed.Items {
|
||||
byID[item.ID] = item
|
||||
}
|
||||
for _, product := range products {
|
||||
detail, err := service.Detail(context.Background(), product.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("detail %d: %v", product.ID, err)
|
||||
}
|
||||
want := byID[product.ID]
|
||||
got := detail.Product
|
||||
if got.CollectionSelectable != want.CollectionSelectable || got.CollectionDisabledReason != want.CollectionDisabledReason {
|
||||
t.Fatalf("detail/list eligibility mismatch for %d: detail=%+v list=%+v", product.ID, got, want)
|
||||
}
|
||||
if (got.ActiveCollectionTaskID == nil) != (want.ActiveCollectionTaskID == nil) {
|
||||
t.Fatalf("detail/list active task mismatch for %d: detail=%+v list=%+v", product.ID, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
<el-alert v-if="location.active && location.goodsId" :title="`当前定位:PDD-${location.productId} / goods_id ${location.goodsId}`" type="info" :closable="false" show-icon class="location-notice" />
|
||||
<p class="muted">已选择 {{ selectedProducts.length }} 条;仅选择当前页,每个商品创建一个独立采集任务,不合并、不拆单。</p>
|
||||
<el-table ref="productTable" v-loading="loading" :data="products" row-key="id" border stripe :empty-text="location.error || '暂无 PDD 商品,请先添加商品链接'" @selection-change="handleSelectionChange">
|
||||
<el-table-column v-if="!location.active" type="selection" width="48" :selectable="isProductSelectable" />
|
||||
<el-table-column type="selection" width="48" :selectable="isProductSelectable" />
|
||||
<el-table-column label="goods_id" prop="goodsId" min-width="150" />
|
||||
<el-table-column label="商品 / 店铺" min-width="240"><template #default="{ row }"><div class="primary">{{ row.title || '资料待完善' }}</div><div class="muted">{{ row.shopName || '尚未填写店铺' }}</div></template></el-table-column>
|
||||
<el-table-column label="状态 / 采集任务" min-width="190"><template #default="{ row }"><el-tag :type="statusMeta(row.status).type">{{ statusMeta(row.status).label }}</el-tag><div v-if="location.active" class="collection-ready">当前定位商品</div><div v-else :class="row.collectionSelectable ? 'collection-ready' : 'muted'">{{ collectionStateText(row) }}</div></template></el-table-column>
|
||||
<el-table-column label="状态 / 采集任务" min-width="190"><template #default="{ row }"><el-tag :type="statusMeta(row.status).type">{{ statusMeta(row.status).label }}</el-tag><div :class="row.collectionSelectable ? 'collection-ready' : 'muted'">{{ collectionStateText(row) }}</div></template></el-table-column>
|
||||
<el-table-column label="销量 / 评价" width="140"><template #default="{ row }"><div>销量 {{ countText(row.salesCount) }}</div><div class="muted">评价 {{ countText(row.reviewCount) }}</div></template></el-table-column>
|
||||
<el-table-column label="规格" min-width="180"><template #default="{ row }">{{ specsSummary(row.specs) }}</template></el-table-column>
|
||||
<el-table-column label="颜色价格" min-width="130"><template #default="{ row }">{{ priceSummary(row.specs) }}</template></el-table-column>
|
||||
|
||||
@@ -47,7 +47,7 @@ test('PDD 商品列表、详情和编辑流程符合已确认原型', async ({ p
|
||||
});
|
||||
|
||||
await page.goto('http://localhost:9527/#/pdd-products/index');
|
||||
await expect(page.getByRole('heading', { name: 'PDD 商品' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '添加', exact: true })).toBeVisible();
|
||||
await expect(page.getByText('夏季防晒冰丝袖套')).toBeVisible();
|
||||
await expect(page.getByText('¥25.90')).toBeVisible();
|
||||
|
||||
@@ -96,6 +96,11 @@ test('带 productId 的 PDD 商品路由进入单商品定位并可通过查询
|
||||
|
||||
await page.getByLabel('PDD 商品详情').getByLabel('关闭此对话框').click();
|
||||
await expect(page.getByLabel('PDD 商品详情')).toBeHidden();
|
||||
const locatedRow = page.locator('.el-table__body-wrapper tbody tr');
|
||||
await expect(locatedRow.getByRole('checkbox')).toBeEnabled();
|
||||
await locatedRow.locator('.el-checkbox').click();
|
||||
await expect(page.getByText('已选择 1 条')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '创建采集 (1)', exact: true })).toBeEnabled();
|
||||
await page.getByRole('button', { name: '查询', exact: true }).click();
|
||||
await expect.poll(() => listCalls).toBe(1);
|
||||
await expect(page).toHaveURL('http://localhost:9527/#/pdd-products/index');
|
||||
@@ -143,6 +148,25 @@ test('单商品定位的重置和无效编号不会误显示其他商品', async
|
||||
await expect(page).toHaveURL('http://localhost:9527/#/pdd-products/index');
|
||||
});
|
||||
|
||||
test('单商品定位对不可采集商品禁用勾选并显示原因', async ({ page, context }) => {
|
||||
const disabled = { ...product, status: 'disabled', collectionSelectable: false, collectionDisabledReason: '商品已停用' };
|
||||
await context.addCookies([{ name: 'Admin-Token', value: 'prototype-test-token', domain: 'localhost', path: '/' }]);
|
||||
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/pdd-products/1')) return route.fulfill({ json: { code: 200, data: { product: disabled } } });
|
||||
return route.fulfill({ json: { code: 200, data: [] } });
|
||||
});
|
||||
|
||||
await page.goto('http://localhost:9527/#/pdd-products/index?productId=1');
|
||||
await page.getByLabel('PDD 商品详情').getByLabel('关闭此对话框').click();
|
||||
await expect(page.getByLabel('PDD 商品详情')).toBeHidden();
|
||||
await expect(page.locator('.el-table__body-wrapper tbody tr').getByRole('checkbox')).toBeDisabled();
|
||||
await expect(page.getByText('商品已停用', { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: '创建采集', exact: true })).toBeDisabled();
|
||||
});
|
||||
|
||||
test('PDD 商品当前页多选并批量创建独立采集任务', async ({ page, context }) => {
|
||||
const second = { ...product, id: 2, goodsId: '719834019024', title: '第二件商品' };
|
||||
const disabled = { ...product, id: 3, goodsId: '731370706977', title: '已停用商品', status: 'disabled', collectionSelectable: false, collectionDisabledReason: '商品已停用' };
|
||||
@@ -169,7 +193,7 @@ test('PDD 商品当前页多选并批量创建独立采集任务', async ({ page
|
||||
await expect(rows.nth(2).getByRole('checkbox')).toBeDisabled();
|
||||
await expect(page.getByText('已选择 2 条')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: '批量创建采集任务' }).click();
|
||||
await page.getByRole('button', { name: '创建采集 (2)', exact: true }).click();
|
||||
await page.getByLabel('采集规则').click();
|
||||
await page.getByRole('option', { name: 'PDD 商品详情采集' }).click();
|
||||
await page.getByRole('button', { name: '创建 2 个采集任务' }).click();
|
||||
|
||||
Reference in New Issue
Block a user