优化模板匹配展示并修复映射迁移
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
-- 037_repair_worker_product_mapping_schema.sql —— 修复旧版商品映射表到多店映射结构。
|
||||
|
||||
ALTER TABLE work_product_rule_mappings
|
||||
ADD COLUMN IF NOT EXISTS seller_ids_json JSONB;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'work_product_rule_mappings'
|
||||
AND column_name = 'seller_id'
|
||||
) THEN
|
||||
UPDATE work_product_rule_mappings
|
||||
SET seller_ids_json = jsonb_build_array(seller_id)
|
||||
WHERE seller_ids_json IS NULL
|
||||
AND seller_id IS NOT NULL
|
||||
AND seller_id <> '';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
ALTER TABLE work_product_rule_mappings
|
||||
ALTER COLUMN seller_ids_json SET DEFAULT '[]'::jsonb,
|
||||
ALTER COLUMN seller_ids_json SET NOT NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = current_schema()
|
||||
AND table_name = 'work_product_rule_mappings'
|
||||
AND column_name = 'seller_id'
|
||||
) THEN
|
||||
ALTER TABLE work_product_rule_mappings
|
||||
ALTER COLUMN seller_id SET DEFAULT '';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
UPDATE work_product_rule_mappings
|
||||
SET mapping_type = 'sku_exact'
|
||||
WHERE mapping_type = 'sku_override';
|
||||
|
||||
DROP INDEX IF EXISTS work_product_rule_mappings_product_identity_unique;
|
||||
DROP INDEX IF EXISTS work_product_rule_mappings_sku_identity_unique;
|
||||
|
||||
ALTER TABLE work_product_rule_mappings
|
||||
DROP CONSTRAINT IF EXISTS work_product_rule_mappings_check,
|
||||
DROP CONSTRAINT IF EXISTS work_product_rule_mappings_check1,
|
||||
DROP CONSTRAINT IF EXISTS work_product_rule_mappings_mapping_type_check,
|
||||
DROP CONSTRAINT IF EXISTS work_product_rule_mappings_seller_id_check,
|
||||
ADD CONSTRAINT work_product_rule_mappings_seller_ids_json_check
|
||||
CHECK (jsonb_typeof(seller_ids_json) = 'array' AND jsonb_array_length(seller_ids_json) > 0),
|
||||
ADD CONSTRAINT work_product_rule_mappings_mapping_type_check
|
||||
CHECK (mapping_type IN ('product_default', 'sku_exact', 'sku_series')),
|
||||
ADD CONSTRAINT work_product_rule_mappings_product_scope_check
|
||||
CHECK (mapping_type IN ('sku_exact', 'sku_series') OR rel_item_id <> '' OR item_title <> ''),
|
||||
ADD CONSTRAINT work_product_rule_mappings_sku_scope_check
|
||||
CHECK (
|
||||
(mapping_type = 'product_default' AND rel_sku_id = '' AND sku_nick = '')
|
||||
OR
|
||||
(mapping_type = 'sku_exact' AND (rel_sku_id <> '' OR sku_nick <> ''))
|
||||
OR
|
||||
(mapping_type = 'sku_series' AND rel_sku_id = '' AND sku_nick <> '')
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS work_product_rule_mappings_seller_ids_idx
|
||||
ON work_product_rule_mappings USING GIN (seller_ids_json);
|
||||
|
||||
DROP INDEX IF EXISTS work_product_rule_mappings_lookup_idx;
|
||||
|
||||
CREATE INDEX work_product_rule_mappings_lookup_idx
|
||||
ON work_product_rule_mappings (rel_item_id, mapping_type, enabled);
|
||||
@@ -29,11 +29,12 @@ import { useState } from 'react'
|
||||
import {
|
||||
deleteAdminWorkProductRule,
|
||||
fetchAdminWorkCategories,
|
||||
fetchAdminWorkProductRuleMappings,
|
||||
fetchAdminWorkProductRules,
|
||||
reprocessAdminKuaishouWorkOrderMatches,
|
||||
saveAdminWorkProductRule,
|
||||
} from '@/services/admin'
|
||||
import type { CollectField, WorkProductRule } from '@/types/worker-platform'
|
||||
import type { CollectField, WorkProductRule, WorkProductRuleMapping } from '@/types/worker-platform'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatMoney } from './shared'
|
||||
|
||||
@@ -59,12 +60,23 @@ export default function ProductRulesPanel() {
|
||||
pageSize,
|
||||
}),
|
||||
})
|
||||
const mappingsQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-product-mappings'],
|
||||
queryFn: () => fetchAdminWorkProductRuleMappings(),
|
||||
})
|
||||
const categoriesQuery = useQuery({
|
||||
queryKey: ['admin-worker-platform-categories'],
|
||||
queryFn: () => fetchAdminWorkCategories(),
|
||||
})
|
||||
|
||||
const rules = rulesQuery.data?.data.items || []
|
||||
const mappings = mappingsQuery.data?.data.items || []
|
||||
const mappingsByRuleId = new Map<number, WorkProductRuleMapping[]>()
|
||||
for (const mapping of mappings) {
|
||||
const ruleMappings = mappingsByRuleId.get(mapping.ruleId) || []
|
||||
ruleMappings.push(mapping)
|
||||
mappingsByRuleId.set(mapping.ruleId, ruleMappings)
|
||||
}
|
||||
const rulesPagination = rulesQuery.data?.data.pagination
|
||||
const categoryCounts = new Map(
|
||||
(rulesQuery.data?.data.categoryCounts || []).map((item) => [
|
||||
@@ -240,6 +252,9 @@ export default function ProductRulesPanel() {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-product-rules'],
|
||||
})
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-product-mappings'],
|
||||
})
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '删除失败')
|
||||
}
|
||||
@@ -288,7 +303,7 @@ export default function ProductRulesPanel() {
|
||||
{
|
||||
title: '模板 / 商品',
|
||||
key: 'rule',
|
||||
width: '29%',
|
||||
width: '34%',
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong title={row.productName || row.ruleKey}>
|
||||
@@ -307,40 +322,10 @@ export default function ProductRulesPanel() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平台 / 店铺',
|
||||
key: 'platformShop',
|
||||
width: '13%',
|
||||
render: (_, row) => {
|
||||
const shopId = String(row.shopId || '')
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text style={{ fontSize: 12 }}>{row.platform || 'kuaishou'}</Typography.Text>
|
||||
{shopId ? (
|
||||
<Tag color="geekblue" style={{ margin: 0, fontSize: 11, lineHeight: '18px' }}>
|
||||
{shopId}
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag style={{ margin: 0, fontSize: 11, lineHeight: '18px' }}>通配店铺</Tag>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'SKU',
|
||||
dataIndex: 'skuCode',
|
||||
width: '11%',
|
||||
render: (value) => (
|
||||
<Typography.Text code style={{ fontSize: 12 }}>
|
||||
{String(value || '-')}
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'categoryName',
|
||||
width: '11%',
|
||||
width: '12%',
|
||||
render: (value) =>
|
||||
value ? (
|
||||
<Tag color="cyan" style={{ margin: 0 }}>
|
||||
@@ -350,10 +335,37 @@ export default function ProductRulesPanel() {
|
||||
<Typography.Text type="secondary">-</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '已匹配商品',
|
||||
key: 'productMappings',
|
||||
width: '18%',
|
||||
render: (_, row) => {
|
||||
const ruleMappings = mappingsByRuleId.get(row.ruleId) || []
|
||||
const enabledCount = ruleMappings.filter((mapping) => mapping.enabled).length
|
||||
const disabledCount = ruleMappings.length - enabledCount
|
||||
return ruleMappings.length > 0 ? (
|
||||
<div className="cell-stack">
|
||||
<Tag
|
||||
color={enabledCount > 0 ? 'green' : 'default'}
|
||||
style={{ margin: 0, width: 'fit-content' }}
|
||||
>
|
||||
{enabledCount > 0 ? `已匹配 ${enabledCount} 条` : '映射已停用'}
|
||||
</Tag>
|
||||
{disabledCount > 0 ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
另有 {disabledCount} 条停用
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<Tag style={{ margin: 0, color: '#8c8c8c' }}>未匹配商品</Tag>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '接单金额',
|
||||
key: 'reward',
|
||||
width: '13%',
|
||||
width: '14%',
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong style={{ color: '#3f8600', fontSize: 13 }}>
|
||||
@@ -370,7 +382,7 @@ export default function ProductRulesPanel() {
|
||||
{
|
||||
title: '匹配',
|
||||
dataIndex: 'matchType',
|
||||
width: '8%',
|
||||
width: '9%',
|
||||
render: (value) => (
|
||||
<Tag color={value === 'exact' ? 'blue' : 'orange'} style={{ margin: 0 }}>
|
||||
{value === 'exact' ? '精确' : '包含'}
|
||||
@@ -380,7 +392,7 @@ export default function ProductRulesPanel() {
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: '6%',
|
||||
width: '7%',
|
||||
render: (_, row) => (
|
||||
<Space direction="vertical" size={2}>
|
||||
<Switch
|
||||
@@ -730,12 +742,18 @@ export default function ProductRulesPanel() {
|
||||
|
||||
<Table<WorkProductRule>
|
||||
rowKey="ruleId"
|
||||
loading={rulesQuery.isLoading}
|
||||
loading={rulesQuery.isLoading || mappingsQuery.isLoading}
|
||||
dataSource={rules}
|
||||
columns={columns}
|
||||
rowClassName={(row) =>
|
||||
editingRule?.ruleId === row.ruleId ? 'product-rule-row-active' : ''
|
||||
}
|
||||
rowClassName={(row) => {
|
||||
const hasMappings = (mappingsByRuleId.get(row.ruleId) || []).length > 0
|
||||
return [
|
||||
editingRule?.ruleId === row.ruleId ? 'product-rule-row-active' : '',
|
||||
hasMappings ? 'product-rule-row-mapped' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
}}
|
||||
onRow={(row) => ({
|
||||
onClick: () => editRule(row),
|
||||
title: '点击编辑此规则',
|
||||
|
||||
@@ -1177,6 +1177,10 @@
|
||||
background: #e6f4ff !important;
|
||||
}
|
||||
|
||||
.product-rule-row-mapped > td {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
/* ===== 后台响应式断点 ===== */
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
|
||||
Reference in New Issue
Block a user