"""销量计算相关函数""" import math from datetime import datetime, timedelta from services.db import query from services.cache import RefreshableCache, register_cache def _build_daily_sales_data(rows): """从SQL查询结果构建 {product_code: {date: qty}} 数据结构""" daily = {} for r in rows: pc = r['货品编号'] d = r['日期'].strftime('%Y-%m-%d') if hasattr(r['日期'], 'strftime') else str(r['日期'])[:10] qty = int(r['日销量']) if r['日销量'] else 0 if pc not in daily: daily[pc] = {} daily[pc][d] = qty return daily 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) def _compute_product_sales(): """计算每个产品的昨日销量、多天平均/最高/最低/标准差""" # 获取最新日期 latest = query("SELECT MAX(日期) AS d FROM daily_outbound")[0]['d'] if hasattr(latest, 'strftime'): latest_str = latest.strftime('%Y-%m-%d') else: latest_str = str(latest)[:10] latest_dt = datetime.strptime(latest_str, '%Y-%m-%d') # 计算各时间范围(扩展到60天) dates_3 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(3)] dates_7 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(7)] dates_14 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(14)] dates_30 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(30)] dates_60 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(60)] # 查询所有产品的每日销量(60天范围) rows = query(""" SELECT 货品编号, 日期, SUM(货品数量) AS 日销量 FROM daily_outbound WHERE 日期 >= %s GROUP BY 货品编号, 日期 """, (dates_60[-1],)) # 构建数据结构 daily = _build_daily_sales_data(rows) result = {} for pc, dmap in daily.items(): yesterday_qty = dmap.get(latest_str, 0) # 各范围数值 qty_3 = [dmap.get(d, 0) for d in dates_3] qty_7 = [dmap.get(d, 0) for d in dates_7] qty_14 = [dmap.get(d, 0) for d in dates_14] qty_30 = [dmap.get(d, 0) for d in dates_30] qty_60 = [dmap.get(d, 0) for d in dates_60] # 平均值 avg_3 = int(round(sum(qty_3) / 3)) avg_7 = int(round(sum(qty_7) / 7)) avg_14 = int(round(sum(qty_14) / 14)) avg_30 = int(round(sum(qty_30) / 30)) avg_60 = int(round(sum(qty_60) / 60)) # 最高值 max_3 = max(qty_3) max_7 = max(qty_7) max_14 = max(qty_14) max_30 = max(qty_30) # 最低值 min_14 = min(qty_14) # 14天标准差 std_14 = int(round(_calc_stddev(qty_14))) # 偏差率计算(防止除零) base = avg_3 if avg_3 != 0 else 1 最低销量偏差率 = round((min_14 - avg_3) / base, 4) 最高销量偏差率 = round((max_14 - avg_3) / base, 4) # 极值判断 极小值 = 0.7 极大值 = 1 if 最低销量偏差率 <= 极小值 and 最高销量偏差率 >= 极大值: 极值判断 = '极大/极小' elif 最低销量偏差率 < 极小值: 极值判断 = '极小' elif 最高销量偏差率 > 极大值: 极值判断 = '极大' else: 极值判断 = '正常' # 14天波动率与波动风险判断 avg_base = avg_14 if avg_14 != 0 else 1 波动率 = round(std_14 / avg_base, 4) if 波动率 <= 0.5: 波动风险 = 'X' elif 波动率 <= 1: 波动风险 = 'Y' else: 波动风险 = 'Z' result[pc] = { '昨日销量': yesterday_qty, '3天平均销量': avg_3, '7天平均销量': avg_7, '14天平均销量': avg_14, '30天平均销量': avg_30, '60天平均销量': avg_60, '3天最高销量': max_3, '7天最高销量': max_7, '14天最高销量': max_14, '30天最高销量': max_30, '14天最低销量': min_14, '14天标准差': std_14, '最低销量偏差率': 最低销量偏差率, '最高销量偏差率': 最高销量偏差率, '极值判断': 极值判断, '14天波动率': 波动率, '波动风险': 波动风险, } return result def _get_warehouse_names_by_property(property_keyword): """获取指定仓库属性标签下的所有仓库名称列表""" rows = query( "SELECT warehouse_name FROM warehouses WHERE FIND_IN_SET(%s, warehouse_property) > 0", (property_keyword,) ) return [r['warehouse_name'] for r in rows if r['warehouse_name']] def _compute_product_sales_cloud_yesterday(): """计算每个产品的云仓昨日销量(仅WDT来源,仓库属性含云仓)""" latest = query("SELECT MAX(日期) AS d FROM daily_outbound")[0]['d'] if hasattr(latest, 'strftime'): latest_str = latest.strftime('%Y-%m-%d') else: latest_str = str(latest)[:10] cloud_wh_names = _get_warehouse_names_by_property('云仓') if not cloud_wh_names: return {} # 清洗仓库名中的Excel公式格式(如="1蝶雅广州新仓(蝶雅自营)" → 1蝶雅广州新仓(蝶雅自营)) rows = query(f""" SELECT 货品编号, SUM(货品数量) AS total_qty FROM daily_outbound WHERE 数据来源 = 'wdt' AND 日期 = %s AND REPLACE(REPLACE(仓库, '="', ''), '"', '') IN ({','.join(['%s']*len(cloud_wh_names))}) GROUP BY 货品编号 """, (latest_str,) + tuple(cloud_wh_names)) result = {} for r in rows: result[r['货品编号']] = int(r['total_qty']) if r['total_qty'] else 0 return result def _compute_product_sales_external_yesterday(): """计算每个产品的外仓昨日销量: - WDT来源且仓库属性含外仓 - tmgj/jdjwt/jdgj/jdbz所有来源(全部算外仓) """ latest = query("SELECT MAX(日期) AS d FROM daily_outbound")[0]['d'] if hasattr(latest, 'strftime'): latest_str = latest.strftime('%Y-%m-%d') else: latest_str = str(latest)[:10] ext_wh_names = _get_warehouse_names_by_property('外仓') ext_wh_ph = ','.join(['%s'] * len(ext_wh_names)) if ext_wh_names else '' # 外仓条件:平台来源全部算外仓 + WDT外仓(仓库名含Excel公式格式需清洗) where_parts = ["数据来源 IN ('tmgj','jdjwt','jdgj','jdbz','dymuto','dymtuo','jdsmj')"] wdt_params = [] if ext_wh_names: clean_sql = "REPLACE(REPLACE(仓库, '=\"', ''), '\"', '')" where_parts.append(f"(数据来源 = 'wdt' AND {clean_sql} IN ({ext_wh_ph}))") wdt_params = list(ext_wh_names) where_sql = '(' + ' OR '.join(where_parts) + ')' params = [latest_str] + wdt_params rows = query(f""" SELECT 货品编号, SUM(货品数量) AS total_qty FROM daily_outbound WHERE 日期 = %s AND {where_sql} GROUP BY 货品编号 """, params) result = {} for r in rows: result[r['货品编号']] = int(r['total_qty']) if r['total_qty'] else 0 return result # 可刷新的缓存实例 sales_cache = RefreshableCache(maxsize=1) register_cache(sales_cache) @sales_cache.wrap def compute_product_sales_cached(): """缓存版销量计算(支持主动刷新)""" return _compute_product_sales() # 云仓/外仓昨日销量缓存 cloud_sales_cache = RefreshableCache(maxsize=1) register_cache(cloud_sales_cache) ext_sales_cache = RefreshableCache(maxsize=1) register_cache(ext_sales_cache) @cloud_sales_cache.wrap def compute_product_sales_cloud_yesterday_cached(): """缓存版云仓昨日销量""" return _compute_product_sales_cloud_yesterday() @ext_sales_cache.wrap def compute_product_sales_external_yesterday_cached(): """缓存版外仓昨日销量""" return _compute_product_sales_external_yesterday() def _get_wh_names_by_property(prop): """获取指定属性的仓库名称列表(含清洗)""" return _get_warehouse_names_by_property(prop) def _compute_product_sales_extended(): """计算总销量 + 云仓销量 + 外仓销量的完整数据结构(含3/7/14天平均等)""" latest = query("SELECT MAX(日期) AS d FROM daily_outbound")[0]['d'] if hasattr(latest, 'strftime'): latest_str = latest.strftime('%Y-%m-%d') else: latest_str = str(latest)[:10] latest_dt = datetime.strptime(latest_str, '%Y-%m-%d') # 云仓/外仓仓库名 cloud_wh = _get_warehouse_names_by_property('云仓') ext_wh = _get_warehouse_names_by_property('外仓') dates_3 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(3)] dates_7 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(7)] dates_14 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(14)] dates_30 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(30)] dates_60 = [(latest_dt - timedelta(days=i)).strftime('%Y-%m-%d') for i in range(60)] # 一次查询所有数据,带来源和仓库信息 rows = query(""" SELECT 货品编号, 日期, 数据来源, REPLACE(REPLACE(仓库, '="', ''), '"', '') AS 仓库名, 货品数量 FROM daily_outbound WHERE 日期 >= %s """, (dates_60[-1],)) # 构建三套数据结构 from collections import defaultdict total_daily = defaultdict(lambda: defaultdict(int)) cloud_daily = defaultdict(lambda: defaultdict(int)) ext_daily = defaultdict(lambda: defaultdict(int)) cloud_wh_set = set(cloud_wh) ext_wh_set = set(ext_wh) ext_sources = {'tmgj', 'jdjwt', 'jdgj', 'jdbz', 'dymuto', 'dymtuo', 'jdsmj'} for r in rows: pc = r['货品编号'] d = r['日期'].strftime('%Y-%m-%d') if hasattr(r['日期'], 'strftime') else str(r['日期'])[:10] qty = int(r['货品数量']) if r['货品数量'] else 0 src = r['数据来源'] wh = r['仓库名'] or '' # 总销量 total_daily[pc][d] += qty # 云仓销量:WDT + 云仓仓库 if src == 'wdt' and wh in cloud_wh_set: cloud_daily[pc][d] += qty # 外仓销量:WDT+外仓仓库 或 4个外仓来源 if (src == 'wdt' and wh in ext_wh_set) or src in ext_sources: ext_daily[pc][d] += qty def _compute_from_daily(daily_map): """从daily map计算销量统计""" result = {} for pc, dmap in daily_map.items(): qty_3 = [dmap.get(d, 0) for d in dates_3] qty_7 = [dmap.get(d, 0) for d in dates_7] qty_14 = [dmap.get(d, 0) for d in dates_14] qty_30 = [dmap.get(d, 0) for d in dates_30] qty_60 = [dmap.get(d, 0) for d in dates_60] result[pc] = { '昨日销量': dmap.get(latest_str, 0), '3天平均销量': int(round(sum(qty_3)/3)), '7天平均销量': int(round(sum(qty_7)/7)), '14天平均销量': int(round(sum(qty_14)/14)), '30天平均销量': int(round(sum(qty_30)/30)), '60天平均销量': int(round(sum(qty_60)/60)), '3天最高销量': max(qty_3), '7天最高销量': max(qty_7), '14天最高销量': max(qty_14), '30天最高销量': max(qty_30), } return result return { 'total': _compute_from_daily(total_daily), 'cloud': _compute_from_daily(cloud_daily), 'external': _compute_from_daily(ext_daily), } # 总+云仓+外仓完整销量缓存 extended_sales_cache = RefreshableCache(maxsize=1) register_cache(extended_sales_cache) @extended_sales_cache.wrap def compute_product_sales_extended_cached(): """缓存版总销量+云仓+外仓完整数据结构""" return _compute_product_sales_extended()