优化模板匹配展示并修复映射迁移

This commit is contained in:
yml2213
2026-08-18 15:04:42 +08:00
parent 7ccf3a6cee
commit cf874b5429
4 changed files with 150 additions and 40 deletions
@@ -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: '点击编辑此规则',
+4
View File
@@ -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) {
+12
View File
@@ -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