26 KiB
计划预测 - 计算列逻辑详解
最后更新:2026-06-23(第二版,精确到代码级细节)
本文档按代码执行路径逐步记录计划预测页面所有计算列的逻辑,不省略任何细节。
一、数据源说明
1.1 销量数据 → daily_outbound 表
取最新日期:
latest = query("SELECT MAX(日期) AS d FROM daily_outbound")[0]['d']
# → latest_str = 格式化后的日期字符串
以最新日期为基准日,逐天向前构造日期列表:
dates_3 = [latest-0d, latest-1d, latest-2d] # 3天
dates_7 = [latest-0d, latest-1d, latest-2d, latest-3d, latest-4d, latest-5d, latest-6d] # 7天
dates_14 = [latest-0d .. latest-13d] # 14天
dates_30 = [latest-0d .. latest-29d] # 30天
dates_60 = [latest-0d .. latest-59d] # 60天
SQL 查询原始数据:
SELECT 货品编号, 日期, SUM(货品数量) AS 日销量
FROM daily_outbound
WHERE 日期 >= dates_60[-1]
GROUP BY 货品编号, 日期
数据整理为:{product_code: {date_string: qty}}
qty = int(r['日销量']) if r['日销量'] else 0
1.2 库存数据 → inventory_detail 表
取最新库存日期:
latest = query("SELECT MAX(data_date) AS d FROM inventory_detail")[0]['d']
库存(排除 15 号仓库):
SELECT product_code, SUM(available_qty) AS total_qty
FROM inventory_detail
WHERE data_date = '{latest}' AND warehouse_code != '15'
GROUP BY product_code
→ inv_map[pc] = int(r['total_qty']) if r['total_qty'] else 0
加工厂库存(仅 15 号仓库):
SELECT product_code, SUM(available_qty) AS total_qty
FROM inventory_detail
WHERE data_date = '{latest}' AND warehouse_code = '15'
GROUP BY product_code
→ inv15_map[pc] = int(r['total_qty']) if r['total_qty'] else 0
生产在途 + 采购在途(所有仓库合计):
SELECT product_code,
SUM(production_in_transit) AS prod_in_transit,
SUM(purchase_in_transit) AS purch_in_transit
FROM inventory_detail
WHERE data_date = '{latest}'
GROUP BY product_code
→ transit_map[pc] = {'生产在途': int(prod_in_transit) or 0, '采购在途': int(purch_in_transit) or 0}
1.3 预测配置 → sales_predict_config 表
SELECT * FROM sales_predict_config ORDER BY id
→ sp_map = {r['trend_type']: r for r in ...}
每条记录字段:trend_type, avg_3d, avg_7d, avg_14d, adjust_factor
1.4 全局配置 → global_config 表
SELECT config_key, config_value FROM global_config
→ gc = {'safety_stock_days': '7', 'suggested_return_days': '15', ...}
默认值(数据库无数据时):
gc = {
'safety_stock_days': '7',
'suggested_return_days': '15',
}
二、销量统计列
2.1 昨日销量
# 取 latest_str(最新日期)当天该产品的出货量
yesterday_qty = dmap.get(latest_str, 0)
类型:整数(int,但最终存储时未额外转换,直接存 Python int)
2.2 3天平均销量
qty_3 = [dmap.get(d, 0) for d in dates_3] # 列表,长度3
avg_3 = int(round(sum(qty_3) / 3))
类型:int,先 round 再 int
2.3 7天平均销量
qty_7 = [dmap.get(d, 0) for d in dates_7] # 列表,长度7
avg_7 = int(round(sum(qty_7) / 7))
2.4 14天平均销量
qty_14 = [dmap.get(d, 0) for d in dates_14] # 列表,长度14
avg_14 = int(round(sum(qty_14) / 14))
2.5 30天平均销量
qty_30 = [dmap.get(d, 0) for d in dates_30] # 列表,长度30
avg_30 = int(round(sum(qty_30) / 30))
2.6 60天平均销量
qty_60 = [dmap.get(d, 0) for d in dates_60] # 列表,长度60
avg_60 = int(round(sum(qty_60) / 60))
2.7 3天最高销量
max_3 = max(qty_3)
2.8 7天最高销量
max_7 = max(qty_7)
2.9 14天最高销量
max_14 = max(qty_14)
2.10 30天最高销量
max_30 = max(qty_30)
2.11 14天最低销量
min_14 = min(qty_14)
2.12 14天标准差
def _calc_stddev(qty_list):
n = len(qty_list)
if n < 2:
return 0
avg = sum(qty_list) / n
variance = sum((x - avg) ** 2 for x in qty_list) / n
return math.sqrt(variance)
std_14 = int(round(_calc_stddev(qty_14)))
公式:
avg = sum(qty_14) / 14
variance = sum((x - avg)² for x in qty_14) / 14 # 总体标准差(分母n,而非n-1)
std = sqrt(variance)
类型:输出为 int(round(...))
2.13 最低销量偏差率
base = avg_3 if avg_3 != 0 else 1
最低销量偏差率 = round((min_14 - avg_3) / base, 4)
公式:(14天最低销量 - 3天平均销量) / max(3天平均销量, 1),保留4位小数
- 注意:
base用的是avg_3(int),如果avg_3 == 0则base = 1 - 不是用
max(avg_3, 1)而是条件赋值 - 由于此时
avg_3是 int,min_14也是 int,除法结果可能是负数
2.14 最高销量偏差率
最高销量偏差率 = round((max_14 - avg_3) / base, 4)
公式:(14天最高销量 - 3天平均销量) / max(3天平均销量, 1),保留4位小数
2.15 极值判断
极小值 = 0.7
极大值 = 1
if 最低销量偏差率 <= 极小值 and 最高销量偏差率 >= 极大值:
极值判断 = '极大/极小'
elif 最低销量偏差率 < 极小值:
极值判断 = '极小'
elif 最高销量偏差率 > 极大值:
极值判断 = '极大'
else:
极值判断 = '正常'
判定细节:
<= 0.7(含等于)算极小;< 0.7(不含等于)才触发"极小">= 1.0(含等于)算极大;> 1.0(不含等于)才触发"极大"- 同时满足两个条件 →
极大/极小 - 优先级:
极大/极小>极小>极大>正常
2.16 14天波动率
avg_base = avg_14 if avg_14 != 0 else 1
波动率 = round(std_14 / avg_base, 4)
公式:14天标准差 / max(14天平均销量, 1),保留4位小数
2.17 波动风险
if 波动率 <= 0.5:
波动风险 = 'X'
elif 波动率 <= 1:
波动风险 = 'Y'
else:
波动风险 = 'Z'
分级:
| 条件 | 结果 |
|---|---|
| 波动率 ≤ 0.5 | X |
| 0.5 < 波动率 ≤ 1.0 | Y |
| 波动率 > 1.0 | Z |
三、销售趋势(10 种)
函数签名
_determine_trend(avg_3, avg_7, avg_14)
调用方式
# 有销量数据时
row['销售趋势'] = _determine_trend(s['3天平均销量'], s['7天平均销量'], s['14天平均销量'])
# 无销量数据时
row['销售趋势'] = _determine_trend(0, 0, 0)
完整逻辑(按代码执行顺序判定)
def _determine_trend(avg_3, avg_7, avg_14):
a3, a7, a14 = float(avg_3), float(avg_7), float(avg_14)
# 规则1:全部为0
if a3 == 0 and a7 == 0 and a14 == 0:
return '近期无销售'
# 防除零
base_7 = a7 if a7 != 0 else 1
base_14 = a14 if a14 != 0 else 1
# 计算比值和差值
ratio_3_7 = a3 / base_7 # 3天/7天 比值
ratio_7_14 = a7 / base_14 # 7天/14天 比值
diff_3_7 = abs(a3 - a7) # 3天与7天的绝对差值
diff_7_14 = abs(a7 - a14) # 7天与14天的绝对差值
# 规则2:开始上升
if a3 > a7 and 0.95 <= ratio_7_14 <= 1.05:
return '开始上升'
# 规则3:开始下跌
if a3 < a7 and 0.95 <= ratio_7_14 <= 1.05:
return '开始下跌'
# 规则4:平稳趋势(差值均 ≤ 20)
if diff_3_7 <= 20 and diff_7_14 <= 20:
return '平稳趋势'
# 规则5:上升趋稳(3天≈7天 且 7天>14天)
if 0.95 <= ratio_3_7 <= 1.05 and a7 > a14:
return '上升趋稳'
# 规则6:下跌趋稳(3天≈7天 且 7天<14天)
if 0.95 <= ratio_3_7 <= 1.05 and a7 < a14:
return '下跌趋稳'
# 规则7:持续上升(3天>7天>14天)
if a3 > a7 and a7 > a14:
return '持续上升'
# 规则8:急跌(3天<7天 且 7天>14天)
if a3 < a7 and a7 > a14:
return '急跌'
# 规则9:持续下跌(3天<7天<14天)
if a3 < a7 and a7 < a14:
return '持续下跌'
# 规则10:急增(3天>7天 且 7天<14天)
if a3 > a7 and a7 < a14:
return '急增'
# 兜底
return '平稳趋势'
注意:
- 所有输入先用
float()转换 0.95 <= ratio <= 1.05表示波动在 ±5% 以内视为"≈"diff <= 20表示绝对差值 ≤ 20 视为"平稳"- 规则有先后顺序:前三条(近期无销售/开始上升/开始下跌)优先匹配,然后才是平稳,最后是各趋势
四、产品分级
函数签名
get_product_grade(monthly_sales, product_name='', product_type='')
调用方式
row['产品分级'] = get_product_grade(
row.get('30天平均销量', 0) * 30, # 月销量 = 30天平均销量 × 30
row.get('product_name', ''),
row.get('产品种类', row.get('product_type', ''))
)
完整逻辑
def get_product_grade(monthly_sales, product_name='', product_type=''):
# 前置过滤1:赠品或含"晒图卡" → 不参与评级
if product_type == '赠品' or '晒图卡' in (product_name or ''):
return '不参与评级'
# 前置过滤2:小样 → 最高C级
if product_type == '小样':
return 'C级'
# 类型转换
try:
ms = float(monthly_sales)
except:
return 'C级'
# 销量分级
if ms >= 10000:
return 'S级'
elif ms >= 5000:
return 'A级'
elif ms >= 3000:
return 'B级'
else:
return 'C级'
注意:
monthly_sales已在外层乘以 30:row.get('30天平均销量', 0) * 30,所以ms是月销量估算值product_type是数据库字段(不是"产品种类"),但外层调用传了row.get('产品种类', row.get('product_type', '')),优先用"产品种类",兜底用数据库字段
五、是否新品
函数签名
is_new_product(created_at_str)
调用方式
row['是否新品'] = is_new_product(row.get('创建时间', ''))
完整逻辑
def is_new_product(created_at_str):
if not created_at_str:
return '返单品'
try:
if isinstance(created_at_str, str):
created = datetime.strptime(created_at_str[:10], '%Y-%m-%d')
else:
created = created_at_str
if hasattr(created, 'strftime'):
five_months_ago = datetime.now() - relativedelta(months=5)
return '新品' if created >= five_months_ago else '返单品'
return '返单品'
except:
return '返单品'
逻辑:创建时间 ≥ 当前时间往前推5个月 → 新品,否则 → 返单品
- 无创建时间或解析失败都返回
返单品
六、产品种类
函数签名
get_product_type_cached(name, brand, db_product_type=None)
调用方式
row['产品种类'] = get_product_type_cached(
row.get('product_name', ''),
row.get('brand', ''),
row.get('product_type')
)
完整逻辑
PACKAGE_KEYWORDS = {
'瓶子', '泵头', '软管', '标签', '标贴', '盖子', '彩盒', '收缩膜',
'阀门', '瓶标', '喷枪', '海绵头', '内料', '膜袋', '唇膏管',
'面膜布', '滴管', '说明书', '卡托', '中盒', '展示盒', '底托', '次抛管',
'旋盖', '瓶体', '礼袋', '管子', '条码', '瓶盖', '丝带', '按摩棒',
'刮板', '刮痧板', '拉菲草', '膜纸', '硅胶刷', '刮勺', '加工', '勺子',
'瓶身', '喷雾头', '瓶贴', '上药器', '内袋', '套膜', '胶头', '塑封膜',
'翻盖', '贴纸', '珍珠棉', '洗眼杯', '棉棒',
}
def _get_product_type(name, brand, db_product_type=None):
if not name:
return '其他'
# 1. 数据库标记为赠品
if db_product_type and db_product_type == '赠品':
return '赠品'
# 2. 名称含包材关键词
for kw in PACKAGE_KEYWORDS:
if kw in name:
return '包材'
# 3. 名称含"小样"
if '小样' in name:
return '小样'
# 4. 有品牌且不为空/无
if brand and brand.strip() and brand.strip() != '无':
return '成品'
# 5. 名称含"赠品"
if '赠品' in name:
return '赠品'
# 6. 数据库标记为成品
if db_product_type and db_product_type in ('成品',):
return db_product_type
# 7. 兜底
return '其他'
判定优先级:赠品(数据库) > 包材(关键词) > 小样(关键词) > 成品(有品牌) > 赠品(关键词) > 成品(数据库兜底) > 其他
七、日销量预测
函数签名
compute_daily_forecast(row, sales_predict_map)
调用方式
row['日销量预测'] = compute_daily_forecast(row, sp_map)
# sp_map = {r['trend_type']: r for r in sales_predict_config_cached()}
完整逻辑
def compute_daily_forecast(row, sales_predict_map):
avg3 = float(row.get('3天平均销量', 0) or 0)
avg7 = float(row.get('7天平均销量', 0) or 0)
avg14 = float(row.get('14天平均销量', 0) or 0)
max30 = float(row.get('30天最高销量', 0) or 0)
trend = row.get('销售趋势', '平稳趋势')
# 如果全部为0 → 无法预测
if avg3 == 0 and avg7 == 0 and avg14 == 0 and max30 == 0:
return 0
# 根据趋势类型获取预测系数
cfg = sales_predict_map.get(trend, {})
w3 = float(cfg.get('avg_3d', 0.5)) # 3天权重,默认0.5
w7 = float(cfg.get('avg_7d', 0.3)) # 7天权重,默认0.3
w14 = float(cfg.get('avg_14d', 0.2)) # 14天权重,默认0.2
adj = float(cfg.get('adjust_factor', 1.0)) # 调节系数,默认1.0
# 加权计算
forecast = (avg3 * w3 + avg7 * w7 + avg14 * w14) * adj
return round(forecast)
注意:
- 所有取值用了
row.get(..., 0) or 0—— 如果值为None或0或'',都统一变0 - 查找配置时用
trend(即"销售趋势"的10种值之一)匹配sales_predict_config.trend_type - 找不到趋势配置时使用默认值(
w3=0.5, w7=0.3, w14=0.2, adj=1.0) - 最后结果
round(forecast)取整
八、支撑天数列(5 列)
全部在 _calc_support_days(row, gc) 中计算
前置处理
safety_days = int(gc.get('safety_stock_days', '7'))
return_days = int(gc.get('suggested_return_days', '15'))
forecast_val = row.get('日销量预测', 0)
fcast = float(forecast_val) if forecast_val not in ('近期无销售', 0, '0') else 0
inv_val = float(row.get('库存', 0) or 0)
inv15_val = float(row.get('加工厂库存', 0) or 0)
prod_transit = float(row.get('生产在途', 0) or 0)
purch_transit = float(row.get('采购在途', 0) or 0)
注意 fcast 的取值逻辑:
fcast = float(forecast_val) if forecast_val not in ('近期无销售', 0, '0') else 0
- 如果
日销量预测的值是字符串'近期无销售'(虽然在预测函数中不会出现,但代码做了兼容),则视为 0 - 如果值是
0或'0',也视为 0
8.1 库存可支撑天数
if fcast == 0:
row['库存可支撑天数'] = 999
else:
row['库存可支撑天数'] = round(inv_val / fcast, 1) if fcast > 0 else 999
公式:库存 / 日销量预测,保留 1 位小数
无销量时:999
8.2 库存工厂可支撑天数(前端显示为"现货采购在途")
if fcast == 0:
row['库存工厂可支撑天数'] = 999
else:
row['库存工厂可支撑天数'] = round(purch_transit / fcast, 1) if fcast > 0 else 999
公式:采购在途 / 日销量预测,保留 1 位小数
8.3 库存工厂待生产可支撑天数(前端显示为"定制品在途")
if fcast == 0:
row['库存工厂待生产可支撑天数'] = 999
else:
row['库存工厂待生产可支撑天数'] = round(prod_transit / fcast, 1) if fcast > 0 else 999
公式:生产在途 / 日销量预测,保留 1 位小数
8.4 加工厂库存支撑天数
if fcast == 0:
row['加工厂库存支撑天数'] = 999
else:
row['加工厂库存支撑天数'] = round(inv15_val / fcast, 1) if fcast > 0 else 999
公式:加工厂库存 / 日销量预测,保留 1 位小数
8.5 总支撑天数
if fcast == 0:
row['总支撑天数'] = 999
else:
total_inv = prod_transit + inv15_val
row['总支撑天数'] = round(total_inv / fcast, 1) if fcast > 0 else 999
公式:(生产在途 + 加工厂库存) / 日销量预测,保留 1 位小数
九、库存健康指数
sup_days = row.get('库存可支撑天数', 999)
s1 = sup_days if isinstance(sup_days, (int, float)) and sup_days != 999 else None
lead_h = float(row.get('生产货期', 0) or 0)
logis_h = float(row.get('物流时间', 0) or 0)
if s1 is not None and (lead_h + logis_h) > 0:
health = s1 / (lead_h + logis_h)
row['库存健康指数'] = round(min(health, 999), 1)
else:
row['库存健康指数'] = 999
逻辑细节:
s1只有当库存可支撑天数是数字类型且不等于 999 时才有效- 即:有销量(支撑天数不为 999)时才计算健康指数
- 公式:
库存可支撑天数 / (生产货期 + 物流时间) - 上限 999:
min(health, 999) - 保留 1 位小数:
round(..., 1) - 分母为 0 时(货期+物流时间为 0)→ 返回 999
十、风险把控
created = row.get('创建时间', '')
sup_days_val = row.get('库存可支撑天数', 999)
try:
sup_days_val = float(sup_days_val)
except:
sup_days_val = 999
days_since_created = 999
try:
if created:
if isinstance(created, str):
created_dt = datetime.strptime(created[:10], '%Y-%m-%d')
else:
created_dt = created
days_since_created = (datetime.now() - created_dt).days
except:
pass
if days_since_created < 90:
row['风险把控'] = '新品'
elif days_since_created > 180 and sup_days_val > 240:
row['风险把控'] = '需启动清库存'
elif days_since_created > 90 and sup_days_val > 90:
row['风险把控'] = '呆滞品'
else:
row['风险把控'] = '正常'
判定表(按代码执行顺序):
| 条件 | 结果 | 说明 |
|---|---|---|
| 创建天数 < 90 | 新品 |
优先匹配 |
| 创建天数 > 180 且 库存可支撑天数 > 240 | 需启动清库存 |
超半年库存过多 |
| 创建天数 > 90 且 库存可支撑天数 > 90 | 呆滞品 |
超过3个月且库存能撑3个月 |
| 其他 | 正常 |
注意:
days_since_created默认 999,如果创建时间解析失败将保持 999- 第一个条件
< 90优先于后面的条件(新品的判断级别最高) days_since_created > 180是严格大于 180(不含等于)sup_days_val > 240和> 90都是严格大于(不含等于)
十一、建议下单数量 & 实际下单数量
11.1 建议下单数量
if fcast == 0 and inv_val > 0:
row['建议下单数量'] = 0
else:
row['建议下单数量'] = int(fcast * (safety_days + lead_h + logis_h))
公式:int(日销量预测 × (安全库存天数 + 生产货期 + 物流时间))
边界条件:
- 如果日销量预测为 0 但库存 > 0 → 结果为 0(有库存但无销量,不下单)
- 如果日销量预测为 0 且库存也为 0 → 进入
else分支,结果 =int(0 * ...)= 0 - 如果日销量预测 > 0 → 正常计算并用
int()截断小数
11.2 实际下单数量
if row['建议下单数量'] == 0:
row['实际下单数量'] = 0
else:
row['实际下单数量'] = ((row['建议下单数量'] + 999) // 1000) * 1000
公式:((建议下单数量 + 999) // 1000) * 1000
效果:按 1000 向上取整
- 1 → 1000
- 999 → 1000
- 1000 → 1000(1000+999=1999//1000=1 → 1×1000=1000)
- 1001 → 2000
- 1234 → 2000
11.3 正式下单数量
- 非自动计算列
- 用户手动编辑输入
- 编辑交互:回车+确认 → 保存到后端 | 取消/Esc/失焦 → 恢复编辑前原值
十二、建议回货数量
total_transit = prod_transit + purch_transit
if fcast > 0 and total_transit > 0:
calc_val = fcast * return_days - inv_val
round_up = int(calc_val * 10 + 0.99999) / 10
if round_up < 0:
round_up = 0
row['建议回货数量'] = round_up if round_up <= total_transit else total_transit
if row['建议回货数量'] < 0:
row['建议回货数量'] = 0
else:
row['建议回货数量'] = 0
公式:
calc_val = 日销量预测 × 建议回货天数 - 库存
round_up = ceil_1(calc_val) # 向上取整到1位小数(通过 int(x * 10 + 0.99999) / 10 实现)
最终值 = min(round_up, 总在途)
最终值 = max(最终值, 0)
注意:
return_days来自全局配置suggested_return_days(默认 15 天)- 向上取整方式:
int(calc_val * 10 + 0.99999) / 10,等价于math.ceil(calc_val * 10) / 10 - 上限为
total_transit(生产在途 + 采购在途的总和) - 下限为 0
- 仅当
fcast > 0 and total_transit > 0时才计算,否则返回 0
十三、本地数据库字段
以下列直接从 products 表(或关联 suppliers 表)读取,不经计算:
SELECT
p.product_code,
p.product_name,
p.brand,
p.product_type,
p.specification,
p.cost_price,
p.lead_time, -- 对应"生产货期"
p.logistics_time, -- 对应"物流时间"
p.min_order_qty, -- 对应"最低起订量"
p.created_at, -- 对应"创建时间"
COALESCE(s.supplier_name, p.supplier_name_match) AS "供应商名称",
p.procurement_follower, -- 对应"采购跟单"
p.procurement_planner, -- 对应"采购计划"
p.operator, -- 对应"运营专员"
p.developer, -- 对应"开发专员"
p.platform, -- 对应"归属平台"
p.category1, -- 对应"一级类目"
p.category2, -- 对应"二级类目"
p.category3, -- 对应"三级类目"
p.bom -- 对应"物料清单"
FROM products p
LEFT JOIN suppliers s ON p.supplier_code = s.supplier_code
十四、注入流程(代码执行路径)
数据从 SQL 取出后,按以下顺序注入每一行:
_enrich_row_with_planning_data(row, sales, inv_map, inv15_map, transit_map, sp_map, gc):
① row['库存'] = inv_map.get(pc, 0)
② row['加工厂库存'] = inv15_map.get(pc, 0)
t = transit_map.get(pc, {})
③ row['生产在途'] = t.get('生产在途', 0)
④ row['采购在途'] = t.get('采购在途', 0)
⑤ 销量数据注入(如果 pc in sales):
- 昨日销量, 3/7/14/30/60天平均销量
- 3/7/14/30天最高销量, 14天最低销量
- 14天标准差, 最低/最高销量偏差率
- 极值判断, 14天波动率, 波动风险
否则全部赋 0
⑥ row['销售趋势'] = _determine_trend(avg3, avg7, avg14)
⑦ row['产品分级'] = get_product_grade(30天平均*30, product_name, 产品种类/产品类型)
⑧ row['是否新品'] = is_new_product(创建时间)
⑨ row['日销量预测'] = compute_daily_forecast(row, sp_map)
⑩ _calc_support_days(row, gc):
- 库存可支撑天数 / 库存工厂可支撑天数 / 库存工厂待生产可支撑天数
- 加工厂库存支撑天数 / 总支撑天数
- 库存健康指数
- 风险把控
- 建议下单数量 / 实际下单数量
- 建议回货数量
附录:依赖关系图
[daily_outbound 表] [inventory_detail 表]
│ │
├─ 昨日销量 ├─ 库存(≠15号仓)
├─ 3/7/14/30/60天平均 ├─ 加工厂库存(15号仓)
├─ 3/7/14/30天最高 ├─ 生产在途
├─ 14天最低 └─ 采购在途
├─ 14天标准差 ──→ 14天波动率 ──→ 波动风险 │
├─ 最低/最高偏差率 ──→ 极值判断 ▼
└─ [平均值] ──→ 销售趋势 [products 表]
│ ├─ 生产货期/物流时间
▼ ├─ 创建时间 ──→ 是否新品
[sales_predict_config] │ └─→ 风险把控
├─ avg_3d/w7/w14 └─ 品牌/产品类型 ──→ 产品分级
└─ adjust_factor │
│ ▼
▼ [产品种类判定]
[日销量预测] (关键词/品牌规则)
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
[支撑天数×5] [库存健康指数] [全局配置]
├─ 库存可支撑天数 = 支撑天数 ├─ safety_stock_days
├─ 库存工厂可支撑 ÷ (货期+物流) ├─ suggested_return_days
├─ 库存工厂待生产可支撑 │
├─ 加工厂库存支撑 ▼
└─ 总支撑天数 [下单数量]
├─ 建议下单数量
├─ 实际下单数量(千位取整)
└─ 建议回货数量