diff --git a/apps/backend/src/db/migrations/037_repair_worker_product_mapping_schema.sql b/apps/backend/src/db/migrations/037_repair_worker_product_mapping_schema.sql new file mode 100644 index 00000000..bb8b6343 --- /dev/null +++ b/apps/backend/src/db/migrations/037_repair_worker_product_mapping_schema.sql @@ -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); diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 312fb636..a2ac8eaa 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -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() + 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) => (
@@ -307,40 +322,10 @@ export default function ProductRulesPanel() {
), }, - { - title: '平台 / 店铺', - key: 'platformShop', - width: '13%', - render: (_, row) => { - const shopId = String(row.shopId || '') - return ( -
- {row.platform || 'kuaishou'} - {shopId ? ( - - {shopId} - - ) : ( - 通配店铺 - )} -
- ) - }, - }, - { - title: 'SKU', - dataIndex: 'skuCode', - width: '11%', - render: (value) => ( - - {String(value || '-')} - - ), - }, { title: '分类', dataIndex: 'categoryName', - width: '11%', + width: '12%', render: (value) => value ? ( @@ -350,10 +335,37 @@ export default function ProductRulesPanel() { - ), }, + { + 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 ? ( +
+ 0 ? 'green' : 'default'} + style={{ margin: 0, width: 'fit-content' }} + > + {enabledCount > 0 ? `已匹配 ${enabledCount} 条` : '映射已停用'} + + {disabledCount > 0 ? ( + + 另有 {disabledCount} 条停用 + + ) : null} +
+ ) : ( + 未匹配商品 + ) + }, + }, { title: '接单金额', key: 'reward', - width: '13%', + width: '14%', render: (_, row) => (
@@ -370,7 +382,7 @@ export default function ProductRulesPanel() { { title: '匹配', dataIndex: 'matchType', - width: '8%', + width: '9%', render: (value) => ( {value === 'exact' ? '精确' : '包含'} @@ -380,7 +392,7 @@ export default function ProductRulesPanel() { { title: '状态', key: 'status', - width: '6%', + width: '7%', render: (_, row) => ( 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: '点击编辑此规则', diff --git a/apps/frontend/src/styles/admin.css b/apps/frontend/src/styles/admin.css index 6c3c085f..199631b3 100644 --- a/apps/frontend/src/styles/admin.css +++ b/apps/frontend/src/styles/admin.css @@ -1177,6 +1177,10 @@ background: #e6f4ff !important; } +.product-rule-row-mapped > td { + background: #f0fdf4; +} + /* ===== 后台响应式断点 ===== */ @media (max-width: 1080px) { diff --git a/deploy/mac-dev.sh b/deploy/mac-dev.sh index eb5294e0..aad4cc72 100755 --- a/deploy/mac-dev.sh +++ b/deploy/mac-dev.sh @@ -293,6 +293,17 @@ start_dev_stack() { 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() { log "等待开发入口健康检查:${HEALTH_URL}" @@ -373,6 +384,7 @@ main() { check_compose_config reset_database start_dev_stack + run_database_migrations wait_for_health print_applied_migrations