优化模板匹配展示并修复映射迁移
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 {
|
import {
|
||||||
deleteAdminWorkProductRule,
|
deleteAdminWorkProductRule,
|
||||||
fetchAdminWorkCategories,
|
fetchAdminWorkCategories,
|
||||||
|
fetchAdminWorkProductRuleMappings,
|
||||||
fetchAdminWorkProductRules,
|
fetchAdminWorkProductRules,
|
||||||
reprocessAdminKuaishouWorkOrderMatches,
|
reprocessAdminKuaishouWorkOrderMatches,
|
||||||
saveAdminWorkProductRule,
|
saveAdminWorkProductRule,
|
||||||
} from '@/services/admin'
|
} 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 { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||||
import { formatMoney } from './shared'
|
import { formatMoney } from './shared'
|
||||||
|
|
||||||
@@ -59,12 +60,23 @@ export default function ProductRulesPanel() {
|
|||||||
pageSize,
|
pageSize,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
const mappingsQuery = useQuery({
|
||||||
|
queryKey: ['admin-worker-platform-product-mappings'],
|
||||||
|
queryFn: () => fetchAdminWorkProductRuleMappings(),
|
||||||
|
})
|
||||||
const categoriesQuery = useQuery({
|
const categoriesQuery = useQuery({
|
||||||
queryKey: ['admin-worker-platform-categories'],
|
queryKey: ['admin-worker-platform-categories'],
|
||||||
queryFn: () => fetchAdminWorkCategories(),
|
queryFn: () => fetchAdminWorkCategories(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = rulesQuery.data?.data.items || []
|
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 rulesPagination = rulesQuery.data?.data.pagination
|
||||||
const categoryCounts = new Map(
|
const categoryCounts = new Map(
|
||||||
(rulesQuery.data?.data.categoryCounts || []).map((item) => [
|
(rulesQuery.data?.data.categoryCounts || []).map((item) => [
|
||||||
@@ -240,6 +252,9 @@ export default function ProductRulesPanel() {
|
|||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ['admin-worker-platform-product-rules'],
|
queryKey: ['admin-worker-platform-product-rules'],
|
||||||
})
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: ['admin-worker-platform-product-mappings'],
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '删除失败')
|
message.error(error instanceof Error ? error.message : '删除失败')
|
||||||
}
|
}
|
||||||
@@ -288,7 +303,7 @@ export default function ProductRulesPanel() {
|
|||||||
{
|
{
|
||||||
title: '模板 / 商品',
|
title: '模板 / 商品',
|
||||||
key: 'rule',
|
key: 'rule',
|
||||||
width: '29%',
|
width: '34%',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text strong title={row.productName || row.ruleKey}>
|
<Typography.Text strong title={row.productName || row.ruleKey}>
|
||||||
@@ -307,40 +322,10 @@ export default function ProductRulesPanel() {
|
|||||||
</div>
|
</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: '分类',
|
title: '分类',
|
||||||
dataIndex: 'categoryName',
|
dataIndex: 'categoryName',
|
||||||
width: '11%',
|
width: '12%',
|
||||||
render: (value) =>
|
render: (value) =>
|
||||||
value ? (
|
value ? (
|
||||||
<Tag color="cyan" style={{ margin: 0 }}>
|
<Tag color="cyan" style={{ margin: 0 }}>
|
||||||
@@ -350,10 +335,37 @@ export default function ProductRulesPanel() {
|
|||||||
<Typography.Text type="secondary">-</Typography.Text>
|
<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: '接单金额',
|
title: '接单金额',
|
||||||
key: 'reward',
|
key: 'reward',
|
||||||
width: '13%',
|
width: '14%',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Typography.Text strong style={{ color: '#3f8600', fontSize: 13 }}>
|
<Typography.Text strong style={{ color: '#3f8600', fontSize: 13 }}>
|
||||||
@@ -370,7 +382,7 @@ export default function ProductRulesPanel() {
|
|||||||
{
|
{
|
||||||
title: '匹配',
|
title: '匹配',
|
||||||
dataIndex: 'matchType',
|
dataIndex: 'matchType',
|
||||||
width: '8%',
|
width: '9%',
|
||||||
render: (value) => (
|
render: (value) => (
|
||||||
<Tag color={value === 'exact' ? 'blue' : 'orange'} style={{ margin: 0 }}>
|
<Tag color={value === 'exact' ? 'blue' : 'orange'} style={{ margin: 0 }}>
|
||||||
{value === 'exact' ? '精确' : '包含'}
|
{value === 'exact' ? '精确' : '包含'}
|
||||||
@@ -380,7 +392,7 @@ export default function ProductRulesPanel() {
|
|||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
key: 'status',
|
key: 'status',
|
||||||
width: '6%',
|
width: '7%',
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space direction="vertical" size={2}>
|
<Space direction="vertical" size={2}>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -730,12 +742,18 @@ export default function ProductRulesPanel() {
|
|||||||
|
|
||||||
<Table<WorkProductRule>
|
<Table<WorkProductRule>
|
||||||
rowKey="ruleId"
|
rowKey="ruleId"
|
||||||
loading={rulesQuery.isLoading}
|
loading={rulesQuery.isLoading || mappingsQuery.isLoading}
|
||||||
dataSource={rules}
|
dataSource={rules}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowClassName={(row) =>
|
rowClassName={(row) => {
|
||||||
editingRule?.ruleId === row.ruleId ? 'product-rule-row-active' : ''
|
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) => ({
|
onRow={(row) => ({
|
||||||
onClick: () => editRule(row),
|
onClick: () => editRule(row),
|
||||||
title: '点击编辑此规则',
|
title: '点击编辑此规则',
|
||||||
|
|||||||
@@ -1177,6 +1177,10 @@
|
|||||||
background: #e6f4ff !important;
|
background: #e6f4ff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-rule-row-mapped > td {
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== 后台响应式断点 ===== */
|
/* ===== 后台响应式断点 ===== */
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
|
|||||||
@@ -293,6 +293,17 @@ start_dev_stack() {
|
|||||||
fail "Mac 本机 Docker 开发环境启动失败,请根据上面的日志排查。"
|
fail "Mac 本机 Docker 开发环境启动失败,请根据上面的日志排查。"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
run_database_migrations() {
|
||||||
|
log "执行数据库迁移"
|
||||||
|
if "${COMPOSE_CMD[@]}" exec -T backend npm run db:migrate; then
|
||||||
|
log "数据库迁移完成"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_diagnostics
|
||||||
|
fail "数据库迁移失败,请根据上面的日志排查。"
|
||||||
|
}
|
||||||
|
|
||||||
wait_for_health() {
|
wait_for_health() {
|
||||||
log "等待开发入口健康检查:${HEALTH_URL}"
|
log "等待开发入口健康检查:${HEALTH_URL}"
|
||||||
|
|
||||||
@@ -373,6 +384,7 @@ main() {
|
|||||||
check_compose_config
|
check_compose_config
|
||||||
reset_database
|
reset_database
|
||||||
start_dev_stack
|
start_dev_stack
|
||||||
|
run_database_migrations
|
||||||
wait_for_health
|
wait_for_health
|
||||||
print_applied_migrations
|
print_applied_migrations
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user