#!/usr/bin/env python3 import argparse import subprocess import sys def _sql_escape(value: str) -> str: return value.replace("'", "''") def build_sql(keyword: str, days: int, limit: int) -> str: kw = _sql_escape(keyword.strip()) return f""" USE [JSDataMiddlePlatform]; SET NOCOUNT ON; DECLARE @kw nvarchar(100) = N'{kw}'; DECLARE @days int = {days}; DECLARE @limit int = {limit}; DECLARE @start_time datetime2 = DATEADD(day, -@days, GETDATE()); DECLARE @end_time datetime2 = GETDATE(); DECLARE @has_exact bit = 0; IF EXISTS (SELECT 1 FROM dbo.jst_sku WITH (NOLOCK) WHERE sku_id = @kw) SET @has_exact = 1; IF OBJECT_ID('tempdb..#matched_sku') IS NOT NULL DROP TABLE #matched_sku; SELECT TOP (300) s.sku_id, s.i_id AS style_code, s.other_1 AS spu, s.name AS sku_name, s.short_name, s.properties_value, s.color, s.item_type, s.category, s.brand, s.vc_name, s.supplier_name, s.supplier_sku_id, s.sku_code, s.sale_price, s.cost_price, s.purchase_price, s.market_price, s.weight, s.l, s.w, s.h, s.unit, s.labels, s.other_2 AS board_type, s.other_10 AS board_material, s.other_3 AS default_warehouse, s.other_5 AS sku_status, s.other_7 AS product_no, s.pic AS preview_pic_url, s.pic_big AS preview_pic_big_url, s.created, s.modified, CASE WHEN s.sku_id = @kw THEN 0 WHEN s.i_id = @kw THEN 1 WHEN s.other_1 = @kw THEN 2 WHEN s.other_7 = @kw THEN 3 WHEN s.sku_id LIKE @kw + '%' THEN 4 WHEN s.name LIKE @kw + '%' THEN 5 ELSE 9 END AS match_rank INTO #matched_sku FROM dbo.jst_sku AS s WITH (NOLOCK) WHERE ISNULL(NULLIF(@kw, ''), '') <> '' AND ( (@has_exact = 1 AND s.sku_id = @kw) OR ( @has_exact = 0 AND ( s.sku_id LIKE '%' + @kw + '%' OR s.i_id LIKE '%' + @kw + '%' OR s.other_1 LIKE '%' + @kw + '%' OR s.name LIKE '%' + @kw + '%' OR s.short_name LIKE '%' + @kw + '%' OR s.other_7 LIKE '%' + @kw + '%' ) ) ) ORDER BY CASE WHEN s.sku_id = @kw THEN 0 WHEN s.i_id = @kw THEN 1 WHEN s.other_1 = @kw THEN 2 WHEN s.other_7 = @kw THEN 3 WHEN s.sku_id LIKE @kw + '%' THEN 4 WHEN s.name LIKE @kw + '%' THEN 5 ELSE 9 END, s.modified DESC; -- 1) 产品基础信息(含SKU名称/成本/材质/预览图 + 扩展字段) SELECT TOP (@limit) sku_id, sku_name, short_name, style_code, spu, product_no, sku_code, properties_value, color, item_type, category, brand, supplier_name, supplier_sku_id, cost_price, purchase_price, sale_price, market_price, board_type, board_material, default_warehouse, sku_status, labels, weight, l, w, h, unit, preview_pic_url, preview_pic_big_url, created, modified FROM #matched_sku ORDER BY match_rank ASC, modified DESC; IF OBJECT_ID('tempdb..#afs') IS NOT NULL DROP TABLE #afs; SELECT NULLIF(a.undefined10, '') AS sku_id, ISNULL(NULLIF(a.undefined36, ''), 'unknown') AS sku_name, ISNULL(NULLIF(a.undefined8, ''), 'unknown') AS shop_name, NULLIF(a.undefined2, '') AS order_id, ISNULL(NULLIF(a.undefined56, ''), '未标注') AS afs_type, ISNULL(NULLIF(a.undefined58, ''), '未标注') AS afs_result, ISNULL(NULLIF(a.undefined57, ''), '') AS native_reason, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined20, '')) AS order_qty, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined30, '')) AS shipped_qty, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined37, '')) AS pre_ship_apply_qty, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined40, '')) AS pre_ship_return_qty, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined46, '')) AS post_ship_apply_qty, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined49, '')) AS post_ship_return_qty, TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined52, '')) AS refund_amount INTO #afs FROM dbo.jst_goods_profit_kpi_items_afs AS a WITH (NOLOCK) WHERE a.createdAt >= @start_time AND a.createdAt < @end_time AND ISNULL(NULLIF(a.undefined2, ''), '') <> '' AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%配件%' AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%特殊单%' AND NULLIF(a.undefined10, '') IN (SELECT sku_id FROM #matched_sku); IF OBJECT_ID('tempdb..#afs_orders') IS NOT NULL DROP TABLE #afs_orders; SELECT DISTINCT order_id INTO #afs_orders FROM #afs WHERE ISNULL(order_id, '') <> ''; IF OBJECT_ID('tempdb..#chat_latest') IS NOT NULL DROP TABLE #chat_latest; WITH chat_rank AS ( SELECT NULLIF(c.orderNo, '') AS order_id, ISNULL(NULLIF(c.mainReason, ''), '') AS main_reason, ISNULL(NULLIF(c.secondReason, ''), '') AS second_reason, ISNULL(NULLIF(c.thirdReason, ''), '') AS third_reason, c.createdAt, ROW_NUMBER() OVER ( PARTITION BY NULLIF(c.orderNo, '') ORDER BY c.createdAt DESC, c.id DESC ) AS rn FROM dbo.pc_kfChatLog AS c WITH (NOLOCK) INNER JOIN #afs_orders AS o ON o.order_id = NULLIF(c.orderNo, '') ) SELECT order_id, main_reason, second_reason, third_reason INTO #chat_latest FROM chat_rank WHERE rn = 1; IF OBJECT_ID('tempdb..#afs_enriched') IS NOT NULL DROP TABLE #afs_enriched; SELECT af.*, CASE WHEN ISNULL(NULLIF(af.native_reason, ''), '') <> '' THEN af.native_reason WHEN ISNULL(NULLIF(cl.main_reason, ''), '') <> '' THEN cl.main_reason ELSE '未标注' END AS reason_l1, CASE WHEN ISNULL(NULLIF(cl.second_reason, ''), '') <> '' THEN cl.second_reason ELSE '未标注' END AS reason_l2, CASE WHEN ISNULL(NULLIF(cl.third_reason, ''), '') <> '' THEN cl.third_reason ELSE '未标注' END AS reason_l3, CASE WHEN ISNULL(NULLIF(af.native_reason, ''), '') <> '' THEN 'afs_reason' WHEN ISNULL(NULLIF(cl.main_reason, ''), '') <> '' THEN 'chat_main_reason' ELSE 'unknown' END AS reason_source INTO #afs_enriched FROM #afs AS af LEFT JOIN #chat_latest AS cl ON cl.order_id = af.order_id; -- 2) 最近N天产品出货/售后件数(件数优先) SELECT TOP (@limit) ae.sku_id, MAX(COALESCE(ms.sku_name, ae.sku_name)) AS sku_name, COUNT(*) AS afs_record_count, COUNT(DISTINCT ae.order_id) AS order_count, SUM(ISNULL(ae.order_qty, 0)) AS order_qty_sum, SUM(ISNULL(ae.shipped_qty, 0)) AS shipped_qty_sum, SUM(CASE WHEN ISNULL(ae.refund_amount, 0) > 0 THEN 1 ELSE 0 END) AS refund_order_count FROM #afs_enriched AS ae LEFT JOIN #matched_sku AS ms ON ms.sku_id = ae.sku_id GROUP BY ae.sku_id ORDER BY shipped_qty_sum DESC, order_count DESC; -- 3) 最近N天该产品的店铺分布(件数) SELECT TOP (@limit) ae.shop_name, COUNT(DISTINCT ae.order_id) AS order_count, SUM(ISNULL(ae.order_qty, 0)) AS order_qty_sum, SUM(ISNULL(ae.shipped_qty, 0)) AS shipped_qty_sum, SUM(CASE WHEN ISNULL(ae.refund_amount, 0) > 0 THEN 1 ELSE 0 END) AS refund_order_count FROM #afs_enriched AS ae GROUP BY ae.shop_name ORDER BY shipped_qty_sum DESC, order_count DESC; IF OBJECT_ID('tempdb..#freight') IS NOT NULL DROP TABLE #freight; SELECT NULLIF(f.sku_id, '') AS sku_id, NULLIF(f.so_id, '') AS so_id, ISNULL(NULLIF(f.receiver_state, ''), '未知') AS receiver_state, CASE WHEN REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ISNULL(NULLIF(f.receiver_state,''),'未知'),'省',''),'市',''),'壮族自治区',''),'回族自治区',''),'维吾尔自治区',''),'自治区',''),'特别行政区',''),'地区','') IN ('上海','江苏','浙江','安徽') THEN '一区' WHEN REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ISNULL(NULLIF(f.receiver_state,''),'未知'),'省',''),'市',''),'壮族自治区',''),'回族自治区',''),'维吾尔自治区',''),'自治区',''),'特别行政区',''),'地区','') IN ('北京','天津','河北','山东','河南','湖北','湖南','江西','福建','广东') THEN '二区' WHEN REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ISNULL(NULLIF(f.receiver_state,''),'未知'),'省',''),'市',''),'壮族自治区',''),'回族自治区',''),'维吾尔自治区',''),'自治区',''),'特别行政区',''),'地区','') IN ('辽宁','吉林','黑龙江','山西','陕西','四川','重庆','贵州','云南','广西','海南','内蒙古','甘肃','宁夏','青海','新疆','西藏') THEN '三区' ELSE '未分区' END AS freight_zone, ISNULL(NULLIF(f.expressName, ''), 'unknown') AS express_name, TRY_CONVERT(decimal(18, 2), f.freightCost) AS freight_cost, TRY_CONVERT(decimal(18, 2), f.weight) AS freight_weight, f.send_date INTO #freight FROM dbo.fys_jstFreightCostDetail AS f WITH (NOLOCK) WHERE f.send_date >= @start_time AND f.send_date < @end_time AND ISNULL(NULLIF(f.sku_id, ''), '') <> '' AND NULLIF(f.sku_id, '') IN (SELECT sku_id FROM #matched_sku); -- 4) 最近N天一区/二区/三区运费(按SKU) SELECT TOP (@limit) fr.sku_id, fr.freight_zone, COUNT(*) AS freight_record_count, COUNT(DISTINCT fr.so_id) AS order_count, SUM(ISNULL(fr.freight_cost, 0)) AS freight_cost_sum, AVG(ISNULL(fr.freight_cost, 0)) AS avg_freight_cost, AVG(ISNULL(fr.freight_weight, 0)) AS avg_weight FROM #freight AS fr GROUP BY fr.sku_id, fr.freight_zone ORDER BY CASE fr.freight_zone WHEN '一区' THEN 1 WHEN '二区' THEN 2 WHEN '三区' THEN 3 ELSE 9 END, order_count DESC; -- 5) 最近N天省份运费TOP(按SKU) SELECT TOP (@limit) fr.sku_id, fr.receiver_state, COUNT(DISTINCT fr.so_id) AS order_count, SUM(ISNULL(fr.freight_cost, 0)) AS freight_cost_sum, AVG(ISNULL(fr.freight_cost, 0)) AS avg_freight_cost, AVG(ISNULL(fr.freight_weight, 0)) AS avg_weight FROM #freight AS fr GROUP BY fr.sku_id, fr.receiver_state ORDER BY order_count DESC, freight_cost_sum DESC; -- 6) 最近N天售后总览(该SKU) SELECT TOP (@limit) ae.sku_id, COUNT(*) AS afs_record_count, COUNT(DISTINCT ae.order_id) AS afs_order_count, SUM(ISNULL(ae.shipped_qty, 0)) AS shipped_qty_sum, SUM(ISNULL(ae.pre_ship_apply_qty, 0) + ISNULL(ae.post_ship_apply_qty, 0)) AS apply_qty_sum, SUM(ISNULL(ae.pre_ship_return_qty, 0) + ISNULL(ae.post_ship_return_qty, 0)) AS return_qty_sum, SUM(CASE WHEN ISNULL(ae.refund_amount, 0) > 0 THEN 1 ELSE 0 END) AS refund_order_count, SUM(ISNULL(ae.refund_amount, 0)) AS refund_amount_sum FROM #afs_enriched AS ae GROUP BY ae.sku_id ORDER BY afs_record_count DESC; -- 7) 最近N天售后类型/结果矩阵(该SKU) SELECT TOP (@limit) ae.sku_id, ae.afs_type, ae.afs_result, COUNT(*) AS afs_record_count, COUNT(DISTINCT ae.order_id) AS order_count, SUM(ISNULL(ae.shipped_qty, 0)) AS shipped_qty_sum, SUM(CASE WHEN ISNULL(ae.refund_amount, 0) > 0 THEN 1 ELSE 0 END) AS refund_order_count FROM #afs_enriched AS ae GROUP BY ae.sku_id, ae.afs_type, ae.afs_result ORDER BY afs_record_count DESC, order_count DESC; -- 8) 最近N天售后原因TOP(该SKU) SELECT TOP (@limit) ae.sku_id, ae.reason_l1, ae.reason_l2, ae.reason_l3, ae.reason_source, COUNT(*) AS afs_record_count, COUNT(DISTINCT ae.order_id) AS order_count, SUM(ISNULL(ae.shipped_qty, 0)) AS shipped_qty_sum, SUM(CASE WHEN ISNULL(ae.refund_amount, 0) > 0 THEN 1 ELSE 0 END) AS refund_order_count FROM #afs_enriched AS ae GROUP BY ae.sku_id, ae.reason_l1, ae.reason_l2, ae.reason_l3, ae.reason_source ORDER BY afs_record_count DESC, order_count DESC; IF OBJECT_ID('tempdb..#inv_latest') IS NOT NULL DROP TABLE #inv_latest; WITH inv_rank AS ( SELECT NULLIF(i.sku_id, '') AS sku_id, TRY_CONVERT(decimal(18, 2), i.qty) AS stock_qty, TRY_CONVERT(decimal(18, 2), i.order_lock) AS order_lock_qty, TRY_CONVERT(decimal(18, 2), i.pick_lock) AS pick_lock_qty, TRY_CONVERT(decimal(18, 2), i.lock_qty) AS lock_qty, TRY_CONVERT(decimal(18, 2), i.virtual_qty) AS virtual_qty, TRY_CONVERT(decimal(18, 2), i.defective_qty) AS defective_qty, TRY_CONVERT(decimal(18, 2), i.purchase_qty) AS purchase_qty, TRY_CONVERT(decimal(18, 2), i.return_qty) AS return_qty, TRY_CONVERT(decimal(18, 2), i.in_qty) AS in_qty, TRY_CONVERT(decimal(18, 2), i.min_qty) AS min_qty, TRY_CONVERT(decimal(18, 2), i.max_qty) AS max_qty, i.wms_co_id, i.name AS inventory_note, COALESCE(i.updateTime, i.modified, i.createTime) AS inventory_snapshot_time, ROW_NUMBER() OVER ( PARTITION BY NULLIF(i.sku_id, '') ORDER BY COALESCE(i.updateTime, i.modified, i.createTime) DESC, i.Id DESC ) AS rn FROM dbo.jst_inventoryInfo AS i WITH (NOLOCK) WHERE NULLIF(i.sku_id, '') IN (SELECT sku_id FROM #matched_sku) ) SELECT sku_id, stock_qty, order_lock_qty, pick_lock_qty, lock_qty, virtual_qty, defective_qty, purchase_qty, return_qty, in_qty, min_qty, max_qty, wms_co_id, inventory_note, inventory_snapshot_time, CASE WHEN ISNULL(stock_qty, 0) - ISNULL(order_lock_qty, 0) - ISNULL(pick_lock_qty, 0) - ISNULL(lock_qty, 0) < 0 THEN CAST(0 AS decimal(18, 2)) ELSE ISNULL(stock_qty, 0) - ISNULL(order_lock_qty, 0) - ISNULL(pick_lock_qty, 0) - ISNULL(lock_qty, 0) END AS available_qty INTO #inv_latest FROM inv_rank WHERE rn = 1; IF OBJECT_ID('tempdb..#ship_base') IS NOT NULL DROP TABLE #ship_base; SELECT NULLIF(a.undefined10, '') AS sku_id, NULLIF(a.undefined2, '') AS order_id, NULLIF(a.undefined12, '') AS product_id, NULLIF(a.undefined11, '') AS style_code_from_afs, NULLIF(a.undefined96, '') AS product_no_from_afs, ISNULL(NULLIF(a.undefined8, ''), 'unknown') AS shop_name, CASE WHEN CHARINDEX('-', ISNULL(NULLIF(a.undefined8, ''), '')) > 0 THEN LEFT(ISNULL(NULLIF(a.undefined8, ''), ''), CHARINDEX('-', ISNULL(NULLIF(a.undefined8, ''), '')) - 1) ELSE ISNULL(NULLIF(a.undefined8, ''), '未知渠道') END AS shop_channel, ISNULL(NULLIF(a.undefined66, ''), '') AS ship_warehouse_name, COALESCE( TRY_CONVERT(datetime2, NULLIF(a.undefined6, ''), 120), TRY_CONVERT(datetime2, REPLACE(NULLIF(a.undefined6, ''), '/', '-'), 120), TRY_CONVERT(datetime2, NULLIF(a.undefined6, ''), 111), TRY_CONVERT(datetime2, a.createdAt) ) AS ship_dt, COALESCE( TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined30, '')), TRY_CONVERT(decimal(18, 2), NULLIF(a.undefined20, '')), CAST(0 AS decimal(18, 2)) ) AS shipped_qty INTO #ship_base FROM dbo.jst_goods_profit_kpi_items_afs AS a WITH (NOLOCK) WHERE NULLIF(a.undefined10, '') IN (SELECT sku_id FROM #matched_sku) AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%配件%' AND ISNULL(NULLIF(a.undefined62, ''), '') NOT LIKE '%特殊单%'; IF OBJECT_ID('tempdb..#ship_daily') IS NOT NULL DROP TABLE #ship_daily; SELECT sb.sku_id, CAST(sb.ship_dt AS date) AS ship_date, COUNT(DISTINCT sb.order_id) AS order_count, SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum INTO #ship_daily FROM #ship_base AS sb WHERE sb.ship_dt >= @start_time AND sb.ship_dt < @end_time GROUP BY sb.sku_id, CAST(sb.ship_dt AS date); IF OBJECT_ID('tempdb..#ship_stats') IS NOT NULL DROP TABLE #ship_stats; SELECT sd.sku_id, SUM(CASE WHEN sd.ship_date >= CAST(DATEADD(day, -7, @end_time) AS date) THEN ISNULL(sd.shipped_qty_sum, 0) ELSE 0 END) AS shipped_qty_7d, SUM(CASE WHEN sd.ship_date >= CAST(DATEADD(day, -30, @end_time) AS date) THEN ISNULL(sd.shipped_qty_sum, 0) ELSE 0 END) AS shipped_qty_30d, MAX(sd.ship_date) AS last_ship_date INTO #ship_stats FROM #ship_daily AS sd GROUP BY sd.sku_id; IF OBJECT_ID('tempdb..#sku_owner') IS NOT NULL DROP TABLE #sku_owner; SELECT ms.sku_id, COALESCE( NULLIF(pc_exact.purchaser, ''), NULLIF(pc_style.purchaser, ''), NULLIF(pc_base.purchaser, ''), '未分配' ) AS purchaser_name, COALESCE( NULLIF(po_exact.oper, ''), NULLIF(po_style.oper, ''), NULLIF(po_base.oper, ''), NULLIF(pc_exact.oper, ''), NULLIF(pc_style.oper, ''), NULLIF(pc_base.oper, ''), '未分配' ) AS spu_oper_name INTO #sku_owner FROM #matched_sku AS ms OUTER APPLY ( SELECT TOP 1 c.purchaser, c.oper FROM dbo.fys_SkuDataLibrary_SpuConfig AS c WITH (NOLOCK) WHERE NULLIF(c.spu, '') = NULLIF(ms.spu, '') ORDER BY c.Id DESC ) AS pc_exact OUTER APPLY ( SELECT TOP 1 c.purchaser, c.oper FROM dbo.fys_SkuDataLibrary_SpuConfig AS c WITH (NOLOCK) WHERE NULLIF(c.spu, '') = NULLIF(ms.style_code, '') ORDER BY c.Id DESC ) AS pc_style OUTER APPLY ( SELECT TOP 1 c.purchaser, c.oper FROM dbo.fys_SkuDataLibrary_SpuConfig AS c WITH (NOLOCK) WHERE NULLIF(c.spu, '') = CASE WHEN CHARINDEX('K', ISNULL(ms.spu, '')) > 0 THEN LEFT(ms.spu, CHARINDEX('K', ms.spu) - 1) ELSE NULL END ORDER BY c.Id DESC ) AS pc_base OUTER APPLY ( SELECT TOP 1 o.oper FROM dbo.op_operManage AS o WITH (NOLOCK) WHERE NULLIF(o.spu, '') = NULLIF(ms.spu, '') ORDER BY o.Id DESC ) AS po_exact OUTER APPLY ( SELECT TOP 1 o.oper FROM dbo.op_operManage AS o WITH (NOLOCK) WHERE NULLIF(o.spu, '') = NULLIF(ms.style_code, '') ORDER BY o.Id DESC ) AS po_style OUTER APPLY ( SELECT TOP 1 o.oper FROM dbo.op_operManage AS o WITH (NOLOCK) WHERE NULLIF(o.spu, '') = CASE WHEN CHARINDEX('K', ISNULL(ms.spu, '')) > 0 THEN LEFT(ms.spu, CHARINDEX('K', ms.spu) - 1) ELSE NULL END ORDER BY o.Id DESC ) AS po_base; IF OBJECT_ID('tempdb..#tm_oper') IS NOT NULL DROP TABLE #tm_oper; WITH tm_rank AS ( SELECT NULLIF(t.shopproid, '') AS product_id, ISNULL(NULLIF(t.shopname, ''), 'unknown') AS shop_name, NULLIF(t.oper, '') AS oper_name, NULLIF(t.dept, '') AS dept_name, ROW_NUMBER() OVER ( PARTITION BY NULLIF(t.shopproid, ''), ISNULL(NULLIF(t.shopname, ''), 'unknown') ORDER BY t.Id DESC ) AS rn FROM dbo.op_TargetManagement AS t WITH (NOLOCK) WHERE NULLIF(t.shopproid, '') IS NOT NULL ) SELECT product_id, shop_name, oper_name, dept_name INTO #tm_oper FROM tm_rank WHERE rn = 1; IF OBJECT_ID('tempdb..#tm_oper_pid') IS NOT NULL DROP TABLE #tm_oper_pid; SELECT product_id, MAX(oper_name) AS oper_name, MAX(dept_name) AS dept_name INTO #tm_oper_pid FROM #tm_oper GROUP BY product_id; IF OBJECT_ID('tempdb..#product_id_contact') IS NOT NULL DROP TABLE #product_id_contact; SELECT sb.sku_id, ms.sku_name, ISNULL(NULLIF(sb.product_id, ''), '(未标注)') AS product_id, sb.shop_channel, sb.shop_name, COALESCE( NULLIF(tm_shop.oper_name, ''), NULLIF(tm_pid.oper_name, ''), NULLIF(ow.spu_oper_name, ''), '未分配' ) AS operator_name, COALESCE(NULLIF(ow.purchaser_name, ''), '未分配') AS purchaser_name, COALESCE(NULLIF(ms.supplier_name, ''), '未标注') AS factory_name, COALESCE(NULLIF(sb.ship_warehouse_name, ''), NULLIF(ms.default_warehouse, ''), NULLIF(inv.inventory_note, ''), '未标注') AS warehouse_name, COUNT(DISTINCT sb.order_id) AS order_count_30d, SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum_30d INTO #product_id_contact FROM #ship_base AS sb INNER JOIN #matched_sku AS ms ON ms.sku_id = sb.sku_id LEFT JOIN #sku_owner AS ow ON ow.sku_id = sb.sku_id LEFT JOIN #tm_oper AS tm_shop ON tm_shop.product_id = NULLIF(sb.product_id, '') AND tm_shop.shop_name = sb.shop_name LEFT JOIN #tm_oper_pid AS tm_pid ON tm_pid.product_id = NULLIF(sb.product_id, '') LEFT JOIN #inv_latest AS inv ON inv.sku_id = sb.sku_id WHERE sb.ship_dt >= @start_time AND sb.ship_dt < @end_time AND ISNULL(NULLIF(sb.product_id, ''), '') <> '' GROUP BY sb.sku_id, ms.sku_name, ISNULL(NULLIF(sb.product_id, ''), '(未标注)'), sb.shop_channel, sb.shop_name, COALESCE( NULLIF(tm_shop.oper_name, ''), NULLIF(tm_pid.oper_name, ''), NULLIF(ow.spu_oper_name, ''), '未分配' ), COALESCE(NULLIF(ow.purchaser_name, ''), '未分配'), COALESCE(NULLIF(ms.supplier_name, ''), '未标注'), COALESCE(NULLIF(sb.ship_warehouse_name, ''), NULLIF(ms.default_warehouse, ''), NULLIF(inv.inventory_note, ''), '未标注'); IF OBJECT_ID('tempdb..#risk_eval') IS NOT NULL DROP TABLE #risk_eval; SELECT ms.sku_id, ms.sku_name, inv.stock_qty, inv.order_lock_qty, inv.pick_lock_qty, inv.lock_qty, inv.available_qty, inv.virtual_qty, inv.defective_qty, inv.purchase_qty, inv.return_qty, inv.in_qty, inv.min_qty, inv.max_qty, inv.wms_co_id, inv.inventory_snapshot_time, ss.last_ship_date, CAST(ISNULL(ss.shipped_qty_7d, 0) / 7.0 AS decimal(18, 2)) AS avg_daily_shipped_7d, CAST(ISNULL(ss.shipped_qty_30d, 0) / 30.0 AS decimal(18, 2)) AS avg_daily_shipped_30d, CAST( CASE WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN NULL ELSE inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0) END AS decimal(18, 2) ) AS estimated_stockout_days, 10 AS replenishment_lead_days, CAST( CASE WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN 0 WHEN inv.available_qty >= (ISNULL(ss.shipped_qty_30d, 0) / 30.0) * 10 THEN 0 ELSE ((ISNULL(ss.shipped_qty_30d, 0) / 30.0) * 10) - inv.available_qty END AS decimal(18, 2) ) AS suggest_replenish_qty_to_10d, CASE WHEN inv.sku_id IS NULL THEN '无库存快照' WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN '近30天无发货' WHEN inv.available_qty <= 0 THEN '缺货/可用库存<=0' WHEN (inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)) <= 10 THEN '补货紧急(<=10天)' WHEN (inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)) <= 15 THEN '补货预警(10-15天)' ELSE '库存安全' END AS stock_risk_level, CASE WHEN inv.sku_id IS NULL THEN 0 WHEN ISNULL(ss.shipped_qty_30d, 0) <= 0 THEN 0 WHEN inv.available_qty <= 0 THEN 1 WHEN (inv.available_qty / NULLIF(ISNULL(ss.shipped_qty_30d, 0) / 30.0, 0)) <= 15 THEN 1 ELSE 0 END AS has_stock_risk INTO #risk_eval FROM #matched_sku AS ms LEFT JOIN #inv_latest AS inv ON inv.sku_id = ms.sku_id LEFT JOIN #ship_stats AS ss ON ss.sku_id = ms.sku_id; -- 9) 最近N天每日总发货件数(按SKU) SELECT TOP (@limit) sd.sku_id, ms.sku_name, sd.ship_date, sd.order_count, sd.shipped_qty_sum FROM #ship_daily AS sd LEFT JOIN #matched_sku AS ms ON ms.sku_id = sd.sku_id ORDER BY sd.ship_date DESC, sd.shipped_qty_sum DESC; -- 10) 库存风险预估(按SKU,补货安全线=10天) SELECT TOP (@limit) re.sku_id, re.sku_name, re.stock_qty, re.order_lock_qty, re.pick_lock_qty, re.lock_qty, re.available_qty, re.virtual_qty, re.defective_qty, re.purchase_qty, re.return_qty, re.in_qty, re.min_qty, re.max_qty, re.wms_co_id, re.inventory_snapshot_time, re.last_ship_date, re.avg_daily_shipped_7d, re.avg_daily_shipped_30d, re.estimated_stockout_days, re.replenishment_lead_days, re.suggest_replenish_qty_to_10d, re.stock_risk_level FROM #risk_eval AS re ORDER BY CASE WHEN re.stock_qty IS NULL THEN 9 WHEN re.avg_daily_shipped_30d <= 0 THEN 8 WHEN re.available_qty <= 0 THEN 1 WHEN re.estimated_stockout_days <= 10 THEN 2 WHEN re.estimated_stockout_days <= 15 THEN 3 ELSE 4 END, re.estimated_stockout_days ASC, re.sku_id ASC; -- 11) 近30天在售商品ID数量(按店铺渠道) SELECT TOP (@limit) sb.sku_id, ms.sku_name, sb.shop_channel, COUNT(DISTINCT sb.product_id) AS sold_product_id_count_30d, COUNT(DISTINCT sb.shop_name) AS shop_count_30d, COUNT(DISTINCT sb.order_id) AS order_count_30d, SUM(ISNULL(sb.shipped_qty, 0)) AS shipped_qty_sum_30d FROM #ship_base AS sb INNER JOIN #matched_sku AS ms ON ms.sku_id = sb.sku_id WHERE sb.ship_dt >= @start_time AND sb.ship_dt < @end_time AND ISNULL(NULLIF(sb.product_id, ''), '') <> '' GROUP BY sb.sku_id, ms.sku_name, sb.shop_channel ORDER BY sold_product_id_count_30d DESC, shipped_qty_sum_30d DESC, sb.shop_channel ASC; -- 12) 商品ID对应店铺/渠道/运营人(用于通知) SELECT TOP (@limit) pic.sku_id, pic.sku_name, pic.product_id, pic.shop_channel, pic.shop_name, pic.operator_name, pic.purchaser_name, pic.factory_name, pic.warehouse_name, pic.order_count_30d, pic.shipped_qty_sum_30d FROM #product_id_contact AS pic ORDER BY pic.shipped_qty_sum_30d DESC, pic.order_count_30d DESC, pic.product_id ASC; -- 13) 缺货风险通知清单(带采购/工厂/仓库/店铺/运营) SELECT TOP (@limit) re.sku_id, re.sku_name, re.stock_risk_level, re.available_qty, re.avg_daily_shipped_30d, re.estimated_stockout_days, re.suggest_replenish_qty_to_10d, pic.product_id, pic.shop_channel, pic.shop_name, pic.operator_name, pic.purchaser_name, pic.factory_name, pic.warehouse_name, pic.shipped_qty_sum_30d FROM #risk_eval AS re LEFT JOIN #product_id_contact AS pic ON pic.sku_id = re.sku_id WHERE re.has_stock_risk = 1 ORDER BY CASE WHEN re.available_qty <= 0 THEN 1 WHEN re.estimated_stockout_days <= 10 THEN 2 WHEN re.estimated_stockout_days <= 15 THEN 3 ELSE 9 END, re.estimated_stockout_days ASC, pic.shipped_qty_sum_30d DESC; """ def main() -> int: parser = argparse.ArgumentParser(description="Company product info lookup (SQLServer)") parser.add_argument("keyword", help="SKU/SPU/款式/产品名关键词") parser.add_argument("--days", type=int, default=30, help="lookback window, default 30") parser.add_argument("--limit", type=int, default=20, help="max rows per result set") args = parser.parse_args() kw = args.keyword.strip() if not kw: print("keyword is required", file=sys.stderr) return 2 days = max(1, min(args.days, 365)) limit = max(1, min(args.limit, 100)) sql = build_sql(kw, days, limit) cmd = ["company-db", "query", "--sql", sql] proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0: err = (proc.stderr or proc.stdout or "company-db failed").strip() print(err, file=sys.stderr) return proc.returncode print(proc.stdout.rstrip()) return 0 if __name__ == "__main__": raise SystemExit(main())