dalongxia/product-info
2026-03-04 13:25:48 +08:00

822 lines
27 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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())