import os
import sys

current_script_dir = os.path.dirname(os.path.abspath(__file__))  # /opt/module/spark/demo/py_demo/dwt
utils_parent_dir = os.path.abspath(os.path.join(current_script_dir, "../"))  # /opt/module/spark/demo/py_demo

# 将 py_demo 加入 Python 搜索路径
sys.path.append(utils_parent_dir)
from functools import reduce

from utils.secure_db_client import get_remote_engine
from pyspark.sql import functions as F

sys.path.append(os.path.dirname(sys.path[0]))  # 上级目录
from pyspark.storagelevel import StorageLevel
from utils.templates import Templates
from utils.hdfs_utils import HdfsUtils          # 落表前清 HDFS 分区目录（幂等重跑）
from utils.common_util import CommonUtil        # 构造 HDFS 分区路径
from yswg_utils.common_udf import udf_new_asin_flag, resolve_asin_jump  # 新品判断UDF + asin跳转判断
# from ..utils.templates import Templates
# from AmazonSpider.pyspark_job.utils.templates_test import Templates
from pyspark.sql.types import StringType, IntegerType, DoubleType, LongType
# 分组排序的udf窗口函数
from pyspark.sql.window import Window
from pyspark.sql import functions as F


class DwtBsTop100(Templates):
    def __init__(self, site_name='us', date_type="month", date_info='2022-01'):
        super().__init__()
        self.site_name = site_name
        self.date_type = date_type
        self.date_info = date_info
        self.db_save = 'dwt_bs_top100'  # 数据库 存储表
        self.spark = self.create_spark_object(
            app_name=f"{self.db_save}: {self.site_name}, {self.date_type}, {self.date_info}")  # spark实例
        self.df_save = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_bs_data = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_deduplicated = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_deduplicated2 = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_cate_bs_data = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_profit_rate = self.spark.sql("select 1+1;")  # DF 对象占位符（利润率 asin+price 粒度）
        self.band_margin_cols = []  # 区间利润率带闸列名(④价格12+⑤月销12=24)，handle_bs_add_column 填充
        self.pkg_band_cols = []     # 打包段"带闸月销列"名(⑥,7段)，handle_bs_add_column 填充
        self.df_cate_tree = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_nodes_num_concat = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.df_nodes_num_concat2 = self.spark.sql("select 1+1;")  # DF 对象占位符
        # ===== 换 node_id 归属新增的占位符 =====
        self.df_node = self.spark.sql("select 1+1;")        # asin→node_id 重定向感知映射（来自 dim_asin_detail）
        self.df_path = self.spark.sql("select 1+1;")        # keepa_category_path：cat_id→node_id_path 整条祖先链
        self.df_cate_name = self.spark.sql("select 1+1;")   # keepa_category：cat_id→name 分类展示名
        self.virtual_node_ids = []                          # 虚拟节点 cat_id 列表（运行时算出，拆层时剔除）
        self.df_level_concat = self.spark.sql("select 1+1;")  # DF 对象占位符
        self.partitions_by = ['site_name', 'date_type', 'date_info']  # 三个分区
        self.udf_new_asin_flag = F.udf(udf_new_asin_flag, IntegerType())  # 新品判断 UDF（与 dim 同一个）
        # 仅月表、且落在历史回刷窗口(24-07~26-06)才按新规则重算 asin_is_new。
        # 起点 24-07=同比基准月(让 25-07 起的新品同比不至于"新规则当月÷旧规则基准")；终点 26-06=历史末月。
        # 窗口外(26-07 及以后)新月份：dim 已是新规则、dwt_flow 冻结值就是对的，不重算直接用。week/4_week 不动。
        # 本次只回刷 us；uk/de 的 keepa 口径未验证，不在范围（未加 site 判断，靠此注释约束运行）。
        self.is_recalc_new = (self.date_type == 'month' and '2024-07' <= self.date_info <= '2026-06')
        self.reset_partitions(partitions_num=1)  # 分区数量

    def read_data(self):

        sql = f"""
                    select asin, asin_type, bsr_orders, category_first_id, category_id, first_category_rank, current_category_rank, 
                    asin_price, asin_rating, asin_buy_box_seller_type, asin_is_new, asin_total_comments, asin_launch_time, asin_launch_time_type, 
                    asin_brand_name, is_brand_label, asin_bought_month as buy_data_bought_month, package_quantity, seller_country_name, account_id
                    from dwt_flow_asin where site_name='{self.site_name}' and date_type='{self.date_type}' and date_info='{self.date_info}'
                """
        print(f"1. 读取dwt_flow_asin数据: sql -- {sql}")
        # 下游线性链只读一次，去掉 cache
        self.df_bs_data = self.spark.sql(sqlQuery=sql)
        # 新品口径 2026-06 迭代为 greatest(上架时间, keepa追踪时间) 判 180 天内；历史 dwt_flow 存的还是旧规则值。
        # 这里旁路读 dim 原始 launch + keepa 快照按新规则重算，对齐到重定向 asin 后覆盖 asin_is_new（详见 recompute_asin_is_new）。
        # coalesce 兜底：极少数只在 measure、dim 无的 asin 退回旧值，绝不写 null（下游对 asin_is_new 做 F.sum，null 会少算）。
        if self.is_recalc_new:
            df_new_flag = self.recompute_asin_is_new()
            self.df_bs_data = self.df_bs_data.join(df_new_flag, on='asin', how='left') \
                .withColumn('asin_is_new', F.coalesce(F.col('asin_is_new_recalc'), F.col('asin_is_new'))) \
                .drop('asin_is_new_recalc')
        # self.df_bs_data.show(10, truncate=False)
        # print("df_bs_data数量", self.df_bs_data.count())
        import pandas as pd
        if pd.__version__ >= "2.0.0" and not hasattr(pd.DataFrame, "iteritems"):
            # 给 DataFrame 动态添加 iteritems 方法，映射到新版 items()
            pd.DataFrame.iteritems = pd.DataFrame.items
        engine = get_remote_engine(site_name="us", db_type="mysql")
        # ============================================================
        # 【旧口径：BSR 自拼树】原来读 us_bs_category（BSR 分类树），配合 handle_bs_hierarchy_to_row 自拼层级。
        #   归属换成页面 node_id 后不再用 BSR 树，整段注释保留备查。
        # sql = f"SELECT nodes_num, category_id, category_parent_id, category_first_id, en_name as cur_category_name from us_bs_category where delete_time is null;"
        # df_cate = engine.read_sql(sql)
        # self.df_cate_bs_data = self.spark.createDataFrame(df_cate)
        # self.df_cate_bs_data.cache()
        # print("df_cate_bs_data数量", self.df_cate_bs_data.count())
        # ============================================================

        # ============================================================
        # 【新口径：keepa 分类树】用 node_id 做归属。两张 MySQL 新表（后端维护）：
        #   keepa_category_path：每个 cat_id 一条，node_id_path=根到叶的 id 链（'>' 分隔，实测样本
        #                        如 "16310091>16310161"），full_path=名称面包屑（'>' 分隔），leaf_node 1叶0非叶。
        #   keepa_category：cat_id→name（分类展示名）。
        # ============================================================
        sql_path = "SELECT cat_id, node_id_path, full_path, leaf_node FROM keepa_category_path;"
        df_path_pd = engine.read_sql(sql_path)
        self.df_path = self.spark.createDataFrame(df_path_pd)
        # cat_id / node_id_path 统一转 string，防 MySQL 数值型与 Spark join 时类型不一致
        self.df_path = self.df_path.withColumn("cat_id", F.col("cat_id").cast("string")) \
                                   .withColumn("node_id_path", F.col("node_id_path").cast("string"))
        self.df_path.cache()
        print("df_path(keepa_category_path)数量", self.df_path.count())

        sql_name = "SELECT cat_id, name FROM keepa_category;"
        df_name_pd = engine.read_sql(sql_name)
        self.df_cate_name = self.spark.createDataFrame(df_name_pd)
        self.df_cate_name = self.df_cate_name.withColumn("cat_id", F.col("cat_id").cast("string"))
        self.df_cate_name.cache()
        print("df_cate_name(keepa_category)数量", self.df_cate_name.count())

        # ============================================================
        # 【虚拟节点识别】node_id_path 里含约 44 个"结构性中转节点"（各部门下的
        #   Categories/Styles/Subjects/Departments 等），Amazon 面包屑不展示。
        # 【识别规则】某 cat_id 的 name ≠ 它自己 full_path 的末段 → 是虚拟节点
        #   （规则与名字无关，别按 'Categories' 字面写死；虚拟节点的名字被 full_path 清掉了，
        #    所以 name 和 full_path 末段对不上）。
        # 【怎么用】算出虚拟 cat_id 列表 → 拆 node_id_path 时用 F.filter 保序剔掉，只留可见层级。
        # ============================================================
        df_vn = self.df_path.select("cat_id", "full_path").join(self.df_cate_name, on="cat_id", how="inner")
        # full_path 末段：按 '>' 切、去两端空格，取最后一段
        df_vn = df_vn.withColumn("_last_name",
                                 F.trim(F.element_at(F.split(F.col("full_path"), ">"), -1)))
        df_vn = df_vn.withColumn("_name_trim", F.trim(F.col("name")))
        df_vn = df_vn.filter(
            F.col("full_path").isNotNull() & (F.col("_name_trim") != F.col("_last_name"))
        )
        self.virtual_node_ids = [r["cat_id"] for r in df_vn.select("cat_id").distinct().collect()]
        print(f"虚拟节点数量={len(self.virtual_node_ids)}，示例={self.virtual_node_ids[:10]}")

        # ============================================================
        # 【取 node_id：重定向感知映射，不改 dwt_flow】
        # 为什么不直接用 dwt_flow 里的 node：dwt_flow 不带 node 列；若拿 dwt_flow 的 asin 回 join
        #   dim_asin_detail 会踩重定向坑——dwt_flow_asin.py 的 handle_asin_redirect 把 asin 换成了
        #   current_asin（跳转目标，判断规则见 resolve_asin_jump），而 dim_asin_detail
        #   存的是原始 asin，直接 join 会大面积假"缺 node"。
        # 做法：读 dim 四小列，复刻 dwt_flow 的重定向变换：key=resolve_asin_jump(asin, current_asin)，
        #   同 key 按 created_time 降序留一条，得到"重定向后 asin → node_id"，键就能和 dwt_flow 的 asin 对上。
        # ============================================================
        sql_node = f"""select asin, current_asin, node_id, created_time
                       from dim_asin_detail
                       where site_name='{self.site_name}' and date_type='{self.date_type}' and date_info='{self.date_info}'"""
        print(f"3. 读取 dim_asin_detail 取 node_id: sql -- {sql_node}")
        df_node = self.spark.sql(sql_node)
        df_node = df_node.withColumn("asin_eff", resolve_asin_jump())
        w_node = Window.partitionBy("asin_eff").orderBy(F.col("created_time").desc_nulls_last())
        df_node = df_node.withColumn("_rk", F.row_number().over(w_node)).filter(F.col("_rk") == 1)
        # 输出成 asin（重定向后）→ node_id，node_id 转 string 与 keepa cat_id 对齐
        self.df_node = df_node.select(
            F.col("asin_eff").alias("asin"),
            F.col("node_id").cast("string").alias("node_id")
        )
        # 【加固】node_id 来自 dim(Hive)，若该列被推断成 double，cast string 会带尾巴 ".0"（如 "123.0"），
        #   与 keepa 的 cat_id "123" join 不上 → 假缺 node 被误排除。这里 strip 掉 ".0"（本月无 .0，纯为未来上保险）。
        self.df_node = self.clean_column(self.df_node, "node_id")
        self.df_node.cache()
        print("df_node(asin→node_id)数量", self.df_node.count())


        # 读利润率数据（dim_asin_profit_rate_info）——给每个 ASIN 补"海运/空运利润率"，
        sql_pr = f"""select asin, price as asin_price, ocean_profit, air_profit
                     from dim_asin_profit_rate_info where site_name='{self.site_name}'"""
        print(f"2. 读取利润率数据: sql -- {sql_pr}")
        self.df_profit_rate = self.spark.sql(sqlQuery=sql_pr).dropDuplicates(['asin', 'asin_price'])

    def recompute_asin_is_new(self):
        """按新规则重算 asin_is_new，产出 (asin=重定向后 target, asin_is_new_recalc)，供 read_data 覆盖。

        新规则 = greatest(上架时间, keepa追踪时间) 距当月最后一天 <=180 天为新品；
        历史 dwt_flow 存的是旧规则（只看上架时间）值。这里从 dim 原始 launch + keepa 快照重算，
        并复刻 dwt_flow 的跳转（coalesce+created_time 最新去重）对齐到重定向 asin，和 df_bs_data 同 grain。
        """
        # 读 dim 原始 launch 及跳转所需列（与 df_node 同一张 dim，单独取以带上 asin_launch_time）
        sql_dim = f"""select asin, current_asin, asin_launch_time, created_time
                      from dim_asin_detail
                      where site_name='{self.site_name}' and date_type='{self.date_type}' and date_info='{self.date_info}'"""
        print(f"读取 dim_asin_detail 重算新品: sql -- {sql_dim}")
        df_dim = self.spark.sql(sql_dim)
        # keepa 追踪起始时间：全量快照不分月，与 dim 新规则同一段口径（tracking_since 分钟级偏移转 yyyy-MM-dd）
        sql_keepa = f"""select asin,
                          date_format(from_unixtime((cast(tracking_since as bigint) + 21564000) * 60), 'yyyy-MM-dd') as keepa_tracking_since
                        from dim_keepa_asin_info
                        where site_name='{self.site_name}' and tracking_since is not null"""
        df_keepa = self.spark.sql(sql_keepa)
        # 按原始 asin 关联 keepa，用新规则重算（与 dim_asin_detail.handle_asin_flag 完全一致）
        cal_date = CommonUtil.get_calDay_by_dateInfo(self.spark, self.date_type, self.date_info)
        df_dim = df_dim.join(df_keepa, on='asin', how='left')
        # keepa 护栏：回刷历史月用的是"今天的 keepa"，其中含追踪时间晚于目标月的记录。
        # 新品 UDF 只有上界(offset<=180)、无下界，未来 keepa 会算出负 offset 被误判成新品(实时算当月时不会遇到、故原 UDF 没这判断)。
        # 这里把晚于当月末的 keepa_tracking_since 置空 → greatest 退回只看上架时间，挡掉误判。
        df_dim = df_dim.withColumn(
            'keepa_tracking_since',
            F.when(F.col('keepa_tracking_since') <= F.lit(cal_date), F.col('keepa_tracking_since')))
        df_dim = df_dim.withColumn(
            "asin_is_new_recalc",
            self.udf_new_asin_flag(F.greatest(F.col('asin_launch_time'), F.col('keepa_tracking_since')), F.lit(cal_date)))
        # 复刻 dwt_flow 跳转：target=resolve_asin_jump(asin, current_asin)，同 target 按 created_time 最新留一条
        df_dim = df_dim.withColumn("asin_eff", resolve_asin_jump())
        w = Window.partitionBy("asin_eff").orderBy(F.col("created_time").desc_nulls_last())
        df_dim = df_dim.withColumn("_rk", F.row_number().over(w)).filter(F.col("_rk") == 1)
        return df_dim.select(F.col("asin_eff").alias("asin"), F.col("asin_is_new_recalc"))

    def clean_column(self, df, col_name):
        """统一处理 ID 列：去除 .0 后缀，处理空值"""
        return df.withColumn(
            col_name,
            F.when(
                F.col(col_name).isNull() |
                (F.col(col_name) == "") |
                (F.col(col_name) == "nan") |
                (F.col(col_name) == "null"),
                None
            ).otherwise(
                F.regexp_replace(F.col(col_name).cast("string"), "\\.0$", "")
            )
        )

    def handle_bs_redirect_flag(self):
        print("handle_bs_redirect_flag调用 分类树按指定列去重（不做重定向）")
        #  根据指定列去重
        drop_col_list = ['category_id', 'category_first_id', 'category_parent_id', 'nodes_num', 'cur_category_name']
        # self.df_cate_bs_data = self.df_cate_bs_data.drop_duplicates(drop_col_list)  # 根据指定列去重
        self.df_cate_bs_data = self.df_cate_bs_data.withColumn("_cate_order",
                                                               F.monotonically_increasing_id())  # 添加一个顺序列 方便去重操作
        w = Window.partitionBy(drop_col_list).orderBy(F.col("_cate_order").asc())
        # 2. 生成行号并过滤
        self.df_cate_bs_data = self.df_cate_bs_data.withColumn("rn", F.row_number().over(w)) \
            .filter(F.col("rn") == 1) \
            .drop("rn")

    def handle_bs_hierarchy_to_row(self):
        print("handle_bs_hierarchy_to_row调用 层级关系处理")
        # print("=== df_cate_bs_data 列名列表 ===")
        # print(self.df_cate_bs_data.columns)
        # 对每个分类的节点进行 层级关系处理 把层级关系转为一行
        nodes_num_list = self.df_cate_bs_data.select("nodes_num").distinct().collect()
        nodes_num_list = [x.nodes_num for x in nodes_num_list]
        nodes_num_list.sort()
        print("nodes_num_list列表",nodes_num_list)
        df_node_list = []
        for num in nodes_num_list[1:]:  # 1为根节点 不需要处理
            num = int(num)
            # print(num)
            df_node = self.df_cate_bs_data.filter(F.col("nodes_num") == num).select("category_id", "category_parent_id",
                                                                                    "_cate_order")
            df_node = df_node.withColumnRenamed("category_id", f"asin_bs_cate_{num - 1}_id").withColumnRenamed(
                "category_parent_id", f"asin_bs_cate_{num - 2}_id")
            df_node = df_node.withColumnRenamed("_cate_order", f"_cate_order_{num - 1}")

            # 开窗函数去重
            window_spec = Window.partitionBy(f"asin_bs_cate_{num - 1}_id", f"asin_bs_cate_{num - 2}_id").orderBy(
                f"_cate_order_{num - 1}")
            df_node = df_node.withColumn("_rn", F.row_number().over(window_spec))
            df_node = df_node.filter(F.col("_rn") == 1).drop("_rn")

            #  num为2 是第一次循环 就把他当成一级分类 父节点 就是根节点 为3 asin_bs_cate_2_id就是二级分类
            # print((df_node.count(), len(df_node.columns)))
            if num == 2:
                self.df_cate_tree = df_node  # 以第一次循环的为基准 此时 df_node 的数据 是asin_bs_cate_1_id asin_bs_cate_0_id 就是一级和根节点
            else:
                #  以父节点为关联键 num =2  是基准 num=3 asin_bs_cate_2_id  asin_bs_cate_1_id  如果以当前循环为关联键 上一次循环的 没有当前的字段
                #  父节点就有 上次循环 父节点和当前循环 的当前节点是一个数据  就会把当前节点的数据 拼接到原基准上
                self.df_cate_tree = self.df_cate_tree.join(df_node, how='left', on=[f"asin_bs_cate_{num - 2}_id"])
            df_node_list.append(df_node)
        #  删除所有 _cate_order 开头的列
        # order_cols = [c for c in self.df_cate_tree.columns if c.startswith("_cate_order")]
        # for col in order_cols:
        #     df_cate_tree = df_cate_tree.drop(col)
        # print("self.df_cate_tree数量", self.df_cate_tree.count())
        print("self.df_cate_tree列名", self.df_cate_tree.columns)
        # print(self.df_cate_tree.show(10, truncate=False))
        print("handle_bs_hierarchy_to_row 层级关系处理完毕")
        self.df_cate_tree.cache()

    def handle_bse_column_rename(self):
        print("handle_bse_column_rename调用 重命名列名")
        # ============================================================
        # 只保留"非分类归属"的列改名：销量(bsr_orders)、BSR一级排名(first_category_rank，废弃保留字段)、
        #   前台月销(buy_data_bought_month)。这些下游还要用。
        # 【换 node 后注释掉的】原来还把 BSR 的 category_id/first/parent 改名成 asin_bs_cate_current_id/1_id/parent_id
        #   当分类归属——现在归属改由 node_id → keepa 路径产出各级 id 列（见 handle_bs_node_path_to_row），
        #   故 BSR 这三列不再参与归属，改名整段注释保留。
        # ============================================================
        rename_dict = {
            'bsr_orders': 'asin_bsr_orders',
            # 'category_id': 'asin_bs_cate_current_id',        # 旧：BSR current 当归属，换 node 后弃用
            # 'category_first_id': 'asin_bs_cate_1_id',        # 旧：BSR 一级当归属，换 node 后弃用
            # 'category_parent_id': 'asin_bs_cate_parent_id',  # 旧：BSR 父级当归属，换 node 后弃用
            'first_category_rank': 'asin_bs_cate_1_rank',
            'buy_data_bought_month': 'asin_amazon_orders'
        }
        for old_col, new_col in rename_dict.items():
            self.df_bs_data = self.df_bs_data.withColumnRenamed(old_col, new_col)
        # 旧：给 BSR 树表 df_cate_bs_data 改名——该表已停用，整段注释保留。
        # rename_dict2 = {
        #     'category_id': 'asin_bs_cate_current_id',
        #     'category_first_id': 'asin_bs_cate_1_id',
        #     'category_parent_id': 'asin_bs_cate_parent_id'
        # }
        # for old, new in rename_dict2.items():
        #     self.df_cate_bs_data = self.df_cate_bs_data.withColumnRenamed(old, new)

    def handle_bs_categorical_data(self):
        print("handle_bs_categorical_data 调用")
        # 排除asin_type not in (1, 2), 1：内部asin，2：视频音乐图书等asin
        print("排除asin_type")
        self.df_bs_data = self.df_bs_data.filter(~(self.df_bs_data.asin_type.isin([1, 2])))
        # ============================================================
        #   下游 handle_bs_nodes_num_concat/concat2 的 cols_select_list、handle_bs_proportion
        #   都写死 launch_time_type0_rate ~ type7_rate。若靠 distinct 造列，某分区恰好 没有某个桶值（如冷门小站点/极早月份无"最近30天上架"=1）→ 该列不生成 →
        #   下游 select 点到不存在的列直接 AnalysisException，整个任务崩。
        #   固定 0~7 造列后，无论数据缺不缺值，8 列恒定存在，缺值时该桶列全为 False → 下游 sum(cast int)=0 → 占比 0/asin_count=0（0%）
        # ============================================================
        print("处理上架时间分类")
        # 处理上架时间分类 上架时间类型(默认0,1:最近30天,2:1-3个月,3:3-6个月,4:6-12个月,5:1-2年,6:2-3年,7:3年以上)
        # launch_time_type_list = [x[0] for x in self.df_bs_data.select("asin_launch_time_type").distinct().collect()]
        # for rate in launch_time_type_list:
        for rate in range(8):
            self.df_bs_data = self.df_bs_data.withColumn(f"launch_time_type{rate}_rate",
                                                         self.df_bs_data["asin_launch_time_type"] == rate)
        #  # 处理卖家类型 buybox卖家类型(1:Amazon,2:FBA,3FBM,4无BB卖家)
        # asin_buy_box_seller_type_list = [x[0] for x in self.df_bs_data.select("asin_buy_box_seller_type").distinct().collect()]
        print("处理卖家类型")
        # for rate in asin_buy_box_seller_type_list:
        for rate in range(5):
            self.df_bs_data = self.df_bs_data.withColumn(f"asin_buy_box_seller_type{rate}_rate",
                                                         self.df_bs_data["asin_buy_box_seller_type"] == rate)
        # 页面只展示 4 档(Amazon/FBA/FBM/其他)，type0(爬虫没抓到卖家信息)并进"其他"，原 type0~type4 五列保留不动。
        # 2024-05 起 dim_asin_detail 走 udf_parse_seller_json，seller_json 空/无 seller_id/解析失败都返回 0，
        # 所以本表跑到的月份 seller_type 无 null，这里等价于 isin(0,4)；
        # 写成"非 1/2/3 即其他"是为了 2024-05 前的老分区（那时用源表原值、可为 null）也能四档互斥穷尽、相加恒=1。
        self.df_bs_data = self.df_bs_data.withColumn(
            "asin_buy_box_seller_type_other_rate",
            F.when(F.col("asin_buy_box_seller_type").isin(1, 2, 3), False).otherwise(True))

        # ============================================================
        # 【卖家所属地占比】 造 3 个布尔列(美/中/其他)，走 rate 路径——handle_bs_level 里 rate_cols
        #   ('rate' in c)自动 sum(cast int)=各国 asin 数，handle_bs_proportion 再 ÷ 分类总 asin 数 = 数量占比。
        # seller_country_name 是国家代码(US/CN/HK/IN/...)，含 null。
        # 【null 归属·必须 null-safe】"其他国家"含 null：
        #   - us/cn 用 when(==,True).otherwise(False)：null 比较得 null → 落 otherwise=False（不进美/中）。
        #   - other 用 when(US或CN, False).otherwise(True)：null → 条件 null → 落 otherwise=True（进其他）。
        #   三者互斥且穷尽(任一 asin 恰好命中一个) → 三列计数和 = asin_count → 三个占比相加=1。
        # ============================================================
        print("处理卖家所属地")
        c = F.col("seller_country_name")
        # 【自营算美国】Amazon 自营(buy_box 卖家类型=1)没有第三方卖家，seller_country_name 为空，
        #   原来全落进"其他国家"(某些类目高达 73%，实为自营)。与选品页对齐：后端对自营固定返回
        #   siteName='us'，故这里也把自营算美国。
        # coalesce 兜底：asin_buy_box_seller_type 为 null 时当作非自营，避免下面 ~is_self 变 null 把该行漏出三档之外。
        is_self = F.coalesce(F.col("asin_buy_box_seller_type") == 1, F.lit(False))
        # 三档必须一起加自营条件、保持互斥穷尽（占比之和恒=1）：
        #   只给美国加的话，"自营+CN" 会同时命中美国和中国，两个占比相加 >1。
        # 旧：只看 seller_country_name，自营(国家空)全进其他
        # self.df_bs_data = self.df_bs_data.withColumn("seller_country_us_rate", F.when(c == "US", True).otherwise(False))
        # self.df_bs_data = self.df_bs_data.withColumn("seller_country_cn_rate", F.when(c == "CN", True).otherwise(False))
        # self.df_bs_data = self.df_bs_data.withColumn("seller_country_other_rate",
        #                                              F.when((c == "US") | (c == "CN"), False).otherwise(True))
        self.df_bs_data = self.df_bs_data.withColumn("seller_country_us_rate",
                                                     F.when(is_self | (c == "US"), True).otherwise(False))
        self.df_bs_data = self.df_bs_data.withColumn("seller_country_cn_rate",
                                                     F.when(~is_self & (c == "CN"), True).otherwise(False))
        self.df_bs_data = self.df_bs_data.withColumn("seller_country_other_rate",
                                                     F.when(is_self | (c == "US") | (c == "CN"), False).otherwise(True))

        # ============================================================
        # 【ASIN 月销 50+/100+/200+ 占比】：月销≥阈值的 asin 数 ÷ 分类总 asin 数(数量占比)。
        #    rate 路径——handle_bs_level 里 rate_cols('rate' in c)自动 sum(cast int)=达标 asin 数，
        #   handle_bs_proportion 再 ÷ asin_count = 占比。
        # 【累计阈值·不互斥】50+ 含 100+ 含 200+，三者不相加=1，各是独立占比。
        # 【null-safe + cast】asin_amazon_orders 是 string，cast double 再比；月销 null → 条件 null →
        #   otherwise(False)→0，算作"不达标"，在分母(asin_count)里、不在分子。此时 handle_bse_column_rename
        #   已把 buy_data_bought_month 改名为 asin_amazon_orders，故这里可直接用。
        # ============================================================
        print("处理月销50+/100+/200+")
        _mo = F.col("asin_amazon_orders").cast("double")
        self.df_bs_data = self.df_bs_data.withColumn("asin_orders_ge_50_rate", F.when(_mo >= 50, True).otherwise(False))
        self.df_bs_data = self.df_bs_data.withColumn("asin_orders_ge_100_rate", F.when(_mo >= 100, True).otherwise(False))
        self.df_bs_data = self.df_bs_data.withColumn("asin_orders_ge_200_rate", F.when(_mo >= 200, True).otherwise(False))

    def handle_bs_asin_deduplicated(self):
        print("handle_bs_asin_deduplicated 调用")
        # # 对每列 转换类型为 string 然后统一将空值  nan null 设置为none  把数据末尾的.0去除
        for col_name in self.df_cate_bs_data.columns:
            if col_name.startswith("_cate_order"):
                continue
            self.df_cate_bs_data = self.df_cate_bs_data.withColumn(col_name, F.col(col_name).cast("string"))
            self.df_cate_bs_data = self.clean_column(self.df_cate_bs_data, col_name)

        # 数据匹配分类数据，并取最小级分类¶ 以asin分类 获取 所有asin分类层级中 最小的节点 后续操作都是对去重去最小分类节点的asin 的df数据操作
        self.df_bs_data = self.df_bs_data.withColumn("asin_bs_cate_current_id",
                                                     self.df_bs_data["asin_bs_cate_current_id"].cast("string"))
        self.df_bs_data = self.df_bs_data.withColumn("asin_bs_cate_current_id",
                                                     F.split(self.df_bs_data["asin_bs_cate_current_id"], "\\.").getItem(
                                                         0))  # 以点分割取第一个

        #  an null 设置为none  把数据末尾的.0去除  以防join出问题
        self.df_bs_data = self.clean_column(self.df_bs_data, "asin_bs_cate_current_id")
        self.df_bs_data = self.clean_column(self.df_bs_data, "asin_bs_cate_1_id")

        self.df_deduplicated = self.df_bs_data.join(self.df_cate_bs_data,
                                                    on=['asin_bs_cate_1_id', 'asin_bs_cate_current_id'], how="left")

        window_spec = Window.partitionBy("asin").orderBy(
            F.col("nodes_num").desc_nulls_last(),  # 降序排列 null在最后
            F.col("_cate_order").asc_nulls_last()
            # nodes_num 相等 按照_cate_order 升序 模拟 Pandas 的 drop_duplicates(keep='first')
        )
        # 生成行号并过滤
        self.df_deduplicated = self.df_deduplicated.withColumn("rn", F.row_number().over(window_spec)) \
            .filter(F.col("rn") == 1) \
            .drop("rn")
        # 下游线性链只读一次，去掉 cache
        # self.df_deduplicated .cache()
        print("df_deduplicated打印")
        # self.df_deduplicated.show(10, truncate=False)
        # print(self.df_deduplicated.columns)
        # print(self.df_deduplicated.count())

    def handle_bs_node_attach(self):
        """
        【替换 handle_bs_asin_deduplicated】给每个 asin 挂 node_id + node_id_path（换 node 归属新逻辑）。
        产出 self.df_deduplicated：一个 asin 一行 + 它的完整祖先链 node_id_path，供 handle_bs_add_column / 拆层用。
        """
        print("handle_bs_node_attach 调用（node_id 归属）")
        # ============================================================
        # Step 1：给 asin 挂 node_id
        # 【为什么 join on asin】df_node 的 key 已按 resolve_asin_jump 复刻了 dwt_flow 的重定向，
        #   而 df_bs_data 的 asin 也是 dwt_flow 重定向后的 asin，两边键对得上。
        # 【dwt_flow 是 asin 粒度】一个 asin 一行，join 后仍一个 asin 一行（df_node 也已按 asin 去重）。
        # ============================================================
        self.df_deduplicated = self.df_bs_data.join(self.df_node, on="asin", how="left")

        # ============================================================
        # Step 2：node_id 为空的直接排除
        # node 空基本是图书/搜索词行（只抓了搜索结果没抓详情页 → 没页面 node、没面包屑），
        # ============================================================
        self.df_deduplicated = self.df_deduplicated.filter(
            F.col("node_id").isNotNull() & (F.trim(F.col("node_id")) != "")
        )

        # ============================================================
        # Step 3：node_id join keepa_category_path 拿整条祖先链 node_id_path
        # ode 命中不了 keepa 树（如 keepa 未收录的冷门 node）也无法归属，一并排除。
        # keepa_category_path 里 cat_id 唯一 → 一个 node 一条链，不会把 asin 扇成多行。
        # 按列名等值 join，避免跨 DF 对象引用列（cat_id）触发列歧义。
        # ============================================================
        df_path_sel = self.df_path.select(
            F.col("cat_id").alias("node_id"), "node_id_path", "leaf_node"
        )
        self.df_deduplicated = self.df_deduplicated.join(df_path_sel, on="node_id", how="left")
        self.df_deduplicated = self.df_deduplicated.filter(
            F.col("node_id_path").isNotNull() & (F.trim(F.col("node_id_path")) != "")
        )

        self.df_deduplicated.cache()
        print("df_deduplicated(挂 node 后)数量", self.df_deduplicated.count())

    def handle_bs_add_column(self):
        print("handle_bs_add_column 调用")
        # 【利润率 join · 在 ASIN 层做一次】此时 df_deduplicated 是"一个 asin 一行
        self.df_deduplicated = self.df_deduplicated.join(self.df_profit_rate, on=['asin', 'asin_price'], how='left')
        self.df_deduplicated = self.df_deduplicated \
            .withColumnRenamed("ocean_profit", "asin_ocean_freight_gross_margin") \
            .withColumnRenamed("air_profit", "asin_air_freight_gross_margin")

        # 销量口径切换：所有"销量"类字段的底层，由"BSR排名预估销量"改为"亚马逊前台真实月销"。 BSR预估 = 排名→查表×系数 猜出来的；同款商品多变体常共享同一排名
        # 把底层列 asin_bsr_orders 直接重定向为 asin_amazon_orders，下游 new/brand/销售额/sum/均值/占比 自动跟着变（字段名不改 避免动 DDL 和下游)
        #  页面不要显示 asin_bsr_orders_sum 这一列（保留字段仅为兼容，避免两列重复展示）
        self.df_deduplicated = self.df_deduplicated.withColumn(
            "asin_bsr_orders", F.col("asin_amazon_orders")
        )

        self.df_deduplicated = self.df_deduplicated.withColumn("asin_bsr_orders_new",
                                                               F.when(self.df_deduplicated.asin_is_new == 1,
                                                                      self.df_deduplicated.asin_bsr_orders).otherwise(
                                                                   F.lit(0)))

        self.df_deduplicated = self.df_deduplicated.withColumn("asin_bsr_orders_brand",
                                                               F.when(self.df_deduplicated.is_brand_label == 1,
                                                                      self.df_deduplicated.asin_bsr_orders).otherwise(
                                                                   F.lit(0)))

        # 旧：level = nodes_num - 1（nodes_num 来自 BSR 树表）。换 node 后无 nodes_num，且该 level 列
        #     未进 cols_select_list、下游未用（handle_bs_level 自己按列名派生 level），整行注释保留。
        # self.df_deduplicated = self.df_deduplicated.withColumn("level", self.df_deduplicated.nodes_num - 1)

        #     销售额相关字段计算
        self.df_deduplicated = self.df_deduplicated.withColumn("asin_bsr_orders_sales",
                                                               self.df_deduplicated.asin_bsr_orders * self.df_deduplicated.asin_price)
        self.df_deduplicated = self.df_deduplicated.withColumn("asin_bsr_orders_sales_new",
                                                               self.df_deduplicated.asin_bsr_orders_new * self.df_deduplicated.asin_price)
        self.df_deduplicated = self.df_deduplicated.withColumn("asin_bsr_orders_sales_brand",
                                                               self.df_deduplicated.asin_bsr_orders_brand * self.df_deduplicated.asin_price)


        # ============================================================
        # 【区间利润率 · 造"带闸列"】：
        #   段内 = 该 asin 利润率、段外 = null；下游 handle_bs_level 对每列 F.avg = 该分类该段的平均利润率。
        # 沿用上游 dwt_flow_asin.asin_price_type 的左闭右开 >=下界 AND <上界。
        #   价格：>0 排除 0/null(价格 0 是脏数据)；10→10-20、80→"80及以上"(>=80)。
        #   月销：字段 asin_amazon_orders(前台月销)实测为干净正整数、无 0；null=前台无徽章，归入 0-50 段
        #        （业务确认：徽章最低档就是 50+，无徽章即月销未达 50；不归的话 0-50 段实测全空）。
        # 【显式 cast("double")】asin_amazon_orders 是 string 类型(前台月销原始文本)，Hive CAST 曾抽风，
        #   Spark 里显式转 double 再比大小更稳；asin_price 同样转一下统一。
        # 【列名】与 整体均值列同源前缀 asin_{ocean|air}_freight_gross_margin + 段后缀；
        #   不能含 "rate"，否则被 handle_bs_level 的 rate_cols('rate' in c)当计数 sum 抓走。
        # ============================================================
        price_col  = F.col("asin_price").cast("double")
        orders_col = F.col("asin_amazon_orders").cast("double")
        band_defs = [
            # 价格区间（后缀 price_*）
            ("price_0_10",  (price_col > 0)  & (price_col < 10)),
            ("price_10_20", (price_col >= 10) & (price_col < 20)),
            ("price_20_30", (price_col >= 20) & (price_col < 30)),
            ("price_30_50", (price_col >= 30) & (price_col < 50)),
            ("price_50_80", (price_col >= 50) & (price_col < 80)),
            ("price_80",    (price_col >= 80)),
            # 月销区间（后缀 orders_*）
            # 旧：null 不满足条件被自动排除 → 0-50 段全空(徽章最低 50+，没有月销<50 的 asin)
            # ("orders_0_50",     (orders_col >= 0)    & (orders_col < 50)),
            # 新：null 一并归入 0-50 段，与 asin_orders_ge_50_rate 把 null 算作"不达标"的口径一致。
            #    用 cast 后的 orders_col 判 null：脏文本 cast 失败也会落这段(实测无脏值)。
            ("orders_0_50",     orders_col.isNull() | ((orders_col >= 0) & (orders_col < 50))),
            ("orders_50_100",   (orders_col >= 50)   & (orders_col < 100)),
            ("orders_100_200",  (orders_col >= 100)  & (orders_col < 200)),
            ("orders_200_500",  (orders_col >= 200)  & (orders_col < 500)),
            ("orders_500_1000", (orders_col >= 500)  & (orders_col < 1000)),
            ("orders_1000",     (orders_col >= 1000)),
        ]
        margin_srcs = [
            ("ocean", "asin_ocean_freight_gross_margin"),
            ("air",   "asin_air_freight_gross_margin"),
        ]
        # 存全部带闸列名到 self（12 + 12 = 24 列），供 cols_select_list / 聚合 / round 三处复用同一份，避免名字漂移
        self.band_margin_cols = []
        for suffix, cond in band_defs:
            for mtype, src in margin_srcs:
                col = f"asin_{mtype}_freight_gross_margin_{suffix}"
                self.df_deduplicated = self.df_deduplicated.withColumn(col, F.when(cond, F.col(src)))
                self.band_margin_cols.append(col)

        # ============================================================
        # 【打包段月销占比 · 造"带闸月销列"】：按打包件数分段，展示"该段月销 ÷ 品类总月销"。
        # 每个打包段造一列：打包件数落本段 = 该 asin 月销(asin_amazon_orders)，否则 null。
        #   下游 handle_bs_level 对每列 F.sum = 该分类该打包段的月销合计；calculate_column 里再 ÷ 品类总月销。
        #   打包件数是整数，左闭右开：1 / [2,5) / [5,10) / [10,20) / [20,50) / [50,100) / [100,+)。
        #   package_quantity 上游 na.fill 到 1、无 null 无 0，故最小段就是 =1。显式 cast("double") 防类型坑。
        # 【命名】中缀 pkg_{段}、列名不含 "rate"(带闸列是月销求和的中间量，不能被 rate_cols 当计数)；
        #   最终占比列在 calculate_column 里生成 _rate 后缀。
        # ============================================================
        pkg_col = F.col("package_quantity").cast("double")
        pkg_bands = [
            ("1",      (pkg_col >= 1)   & (pkg_col < 2)),
            ("2_4",    (pkg_col >= 2)   & (pkg_col < 5)),
            ("5_10",   (pkg_col >= 5)   & (pkg_col < 10)),
            ("10_20",  (pkg_col >= 10)  & (pkg_col < 20)),
            ("20_50",  (pkg_col >= 20)  & (pkg_col < 50)),
            ("50_100", (pkg_col >= 50)  & (pkg_col < 100)),
            ("100",    (pkg_col >= 100)),
        ]
        self.pkg_band_cols = []
        for suffix, cond in pkg_bands:
            col = f"asin_pkg_{suffix}_orders_sum"
            self.df_deduplicated = self.df_deduplicated.withColumn(col, F.when(cond, F.col("asin_amazon_orders")))
            self.pkg_band_cols.append(col)

    # 注：handle_bs_nodes_num_concat（非2版，2026-07-02 删除）——早期草稿，缺 fillna(nodes_num,-1) 会丢 null 分类的 asin、
    #     用 dropDuplicates 去重不确定，且结果 df_nodes_num_concat 从未被下游消费（空跑浪费）。正式逻辑见下方 concat2。
    #     如需找回：开发母本 spark_hjm/bs_top100/dwt_bs_top100.py 或 git 历史。

    def handle_bs_node_path_to_row(self):
        """
        【替换 handle_bs_nodes_num_concat2】把 node_id_path 拆成各级分类 id 列，产出下游拆层要的宽表。
        产出 self.df_nodes_num_concat2：一个 asin 一行 + asin_bs_cate_1_id..N_id（各级祖先 id）+ 各指标列，
        列结构与老 concat2 完全一致，故 handle_bs_level 起下游一行不用改。
        cat_id 唯一→无 DAG 扇出，老 concat2 里按深度分桶、tree join 回补、parent_id_join tie-break 全不需要。
        """
        print("handle_bs_node_path_to_row 调用（拆 node_id_path 为各级 id 列）")

        # ============================================================
        # Step 1：node_id_path 按 '>' 切成 id 数组，并剔掉空串 + 虚拟节点
        # 【为什么剔虚拟节点】node_id_path 含 Categories/Styles 等结构性中转节点，Amazon 面包屑不展示，
        #   剔掉后剩下的才是可见层级；剔完的 id 数组应与 full_path（可见面包屑）逐段对齐。
        # 【为什么用 F.filter 不用 array_except】F.filter 保证保留原顺序（根→叶），array_except 不保证序。
        # 例：node_id_path="1000>2000>9999>3000"（9999=虚拟）→ ["1000","2000","3000"]
        # ============================================================
        arr_raw = F.split(F.col("node_id_path"), ">")
        if self.virtual_node_ids:
            cate_arr = F.filter(
                arr_raw,
                lambda x: (F.trim(x) != "") & (~x.isin(self.virtual_node_ids))
            )
        else:
            cate_arr = F.filter(arr_raw, lambda x: F.trim(x) != "")
        df = self.df_deduplicated.withColumn("_cate_arr", cate_arr)

        # ============================================================
        # Step 2：算出本月最大层级深度，据此把数组按位置摊成 asin_bs_cate_1_id..N_id
        # 【为什么要先求 maxd】各 asin 深度不一（有的 3 级有的 6 级），要建到全月最深的那级，浅的 asin 深层列自动为 null。
        # 【element_at 1 基、越界返 null】asin_bs_cate_i_id = 数组第 i 个；i 超过该 asin 深度 → null（正是上卷要的：深层为空）。
        # 【asin_bs_cate_1_id = 数组第1个 = 部门（可见一级）】剔虚拟节点后首段即部门，对齐老口径的一级。
        # ============================================================
        maxd = df.agg(F.max(F.size("_cate_arr")).alias("m")).first()["m"]
        maxd = int(maxd) if maxd else 0
        print(f"本月最大分类层级深度 maxd={maxd}")
        for i in range(1, maxd + 1):
            df = df.withColumn(f"asin_bs_cate_{i}_id", F.element_at(F.col("_cate_arr"), i).cast("string"))
            # 复用老逻辑的清洗：空串/nan/null 归 None、去末尾 .0，防后续 join/分组出问题
            df = self.clean_column(df, f"asin_bs_cate_{i}_id")

        # ============================================================
        # Step 3：只保留下游要的列（指标列 + 拆出来的各级 id 列），对齐老 concat2 的输出结构
        # 【与老 cols_select_list 的差异】去掉了 BSR 的 asin_bs_cate_current_id / asin_bs_cate_1_id / nodes_num
        #   （current_id/parent_id 由 handle_bs_level 每层现场派生，1_id 已由上面拆层产出）。其余指标列原样保留。
        # ============================================================
        metric_cols = ['asin', 'asin_amazon_orders', 'asin_bsr_orders', 'asin_bsr_orders_new',
                       'asin_bsr_orders_brand',
                       'asin_bsr_orders_sales', 'asin_bsr_orders_sales_new', 'asin_bsr_orders_sales_brand',
                       'asin_bs_cate_1_rank', 'asin_price', 'asin_rating', 'asin_is_new', 'is_brand_label',
                       'asin_total_comments',
                       'launch_time_type0_rate', 'launch_time_type1_rate', 'launch_time_type2_rate',
                       'launch_time_type3_rate', 'launch_time_type4_rate',
                       'launch_time_type5_rate', 'launch_time_type6_rate', 'launch_time_type7_rate',
                       'asin_buy_box_seller_type0_rate', 'asin_buy_box_seller_type1_rate',
                       'asin_buy_box_seller_type2_rate', 'asin_buy_box_seller_type3_rate',
                       'asin_buy_box_seller_type4_rate',
                       #  卖家类型"其他"档(type0+type4+null 合并)，带进拆行 select 才能活到 rate 路径求占比
                       'asin_buy_box_seller_type_other_rate',
                       'seller_country_us_rate', 'seller_country_cn_rate', 'seller_country_other_rate',
                       'asin_orders_ge_50_rate', 'asin_orders_ge_100_rate', 'asin_orders_ge_200_rate',
                       'asin_brand_name', 'account_id',
                       'asin_ocean_freight_gross_margin', 'asin_air_freight_gross_margin']
        metric_cols = metric_cols + self.band_margin_cols + self.pkg_band_cols
        id_cols = [f"asin_bs_cate_{i}_id" for i in range(1, maxd + 1)]
        self.df_nodes_num_concat2 = df.select(*(metric_cols + id_cols))
        self.df_nodes_num_concat2.cache()
        print("df_nodes_num_concat2(拆层后)列名", self.df_nodes_num_concat2.columns)

    def handle_bs_nodes_num_concat2(self):
        print("handle_bs_nodes_num_concat2 打印")
        self.df_deduplicated2 = self.df_deduplicated.fillna({"nodes_num": -1})
        self.df_deduplicated2.cache()
        print("df_deduplicated2数量",self.df_deduplicated2.count())
        raw_list = self.df_deduplicated2.filter(F.col("nodes_num").isNotNull()).agg(F.collect_set("nodes_num")).first()[0]
        nodes_num_list2 = sorted([int(x) for x in raw_list])
        print("df_node_num_list2打印", nodes_num_list2)

        cols_select_list = ['asin', 'asin_amazon_orders', 'asin_bsr_orders', 'asin_bsr_orders_new',
                            'asin_bsr_orders_brand',
                            'asin_bsr_orders_sales', 'asin_bsr_orders_sales_new', 'asin_bsr_orders_sales_brand',
                            'asin_bs_cate_1_rank', 'asin_price', 'asin_rating', 'asin_is_new', 'is_brand_label',
                            'asin_total_comments', 'asin_bs_cate_current_id', 'asin_bs_cate_1_id',
                            'launch_time_type0_rate', 'launch_time_type1_rate', 'launch_time_type2_rate',
                            'launch_time_type3_rate', 'launch_time_type4_rate',
                            'launch_time_type5_rate', 'launch_time_type6_rate', 'launch_time_type7_rate',
                            'asin_buy_box_seller_type0_rate', 'asin_buy_box_seller_type1_rate',
                            'asin_buy_box_seller_type2_rate', 'asin_buy_box_seller_type3_rate',
                            'asin_buy_box_seller_type4_rate',
                            #  卖家所属地 3 个布尔列，带进拆行 select 才能活到 rate 路径求占比
                            'seller_country_us_rate', 'seller_country_cn_rate', 'seller_country_other_rate',
                            #  月销50+/100+/200+ 3 个布尔列，同走 rate 路径求数量占比
                            'asin_orders_ge_50_rate', 'asin_orders_ge_100_rate', 'asin_orders_ge_200_rate',
                            #  集中度要用的品牌名、卖家id（在 handle_bs_level 里做 top10 排序）
                            'asin_brand_name', 'account_id',
                            # 利润率两列（asin 层已 join），带进拆行 select 才能活到分层聚合求平均
                            'asin_ocean_freight_gross_margin', 'asin_air_freight_gross_margin',
                            'nodes_num']
        # 区间利润率的 24 个带闸列(价格12+月销12)同样带进拆行 select，才能活到 handle_bs_level 求段内平均
        cols_select_list = cols_select_list + self.band_margin_cols
        # 打包段的 7 个带闸月销列同样带进拆行 select，才能活到 handle_bs_level 求段内月销合计
        cols_select_list = cols_select_list + self.pkg_band_cols
        df_node_num_list2 = []
        for idx,num in enumerate(nodes_num_list2) :
            num = int(num)
            df_node_num = self.df_deduplicated2.filter(self.df_deduplicated2.nodes_num == num).select(*cols_select_list)
            df_node_num = df_node_num.withColumn("_loop_order", F.lit(idx))
            # print(num,df_node_num.count(),len(df_node_num.columns))
            # ============================================================
            # num==2 = 这个 ASIN 最深的分类就是"一级"本身（nodes_num：根=1、一级=2、二级=3…）。
            # 此时 current_id 和 cols_select_list 里已选入的 asin_bs_cate_1_id 指向同一个分类。
            # 若走通用逻辑（下面 elif）把 current_id 改名成 asin_bs_cate_{num-1}_id：num-1=1 →
            # 变成 asin_bs_cate_1_id，和已有的那列撞名 → 出现两列同名 → 后面 unionByName报 "duplicate column asin_bs_cate_1_id" 直接崩。
            # 一级没有更上层祖先、不需要 join 分类树补中间层，把多余的 current_id  drop 掉即可；该 ASIN 靠已有的 asin_bs_cate_1_id 正常进入 level=1 聚合。
            # 例：某 ASIN 最深就挂在 electronics（nodes_num=2）→ 保留 asin_bs_cate_1_id=electronics、drop current_id。
            # ============================================================
            if num == 2:
                df_node_num = df_node_num.drop("asin_bs_cate_current_id")
            elif num > 2:
                df_node_num = df_node_num.withColumnRenamed("asin_bs_cate_current_id", f"asin_bs_cate_{num - 1}_id")
                cols_list = [f"asin_bs_cate_{i}_id" for i in
                             range(1, num)]  # 生成当前分类的层级路径 如 num=4 生成 1-3的asin_bs_cate_x_id

                # df_first_tree2 = self.df_cate_tree.select(*cols_list).dropDuplicates()
                # 获取分类树中对应的列 _cate_order_1  _cate_order_2 这种
                order_cols = [c for c in self.df_cate_tree.columns if c.startswith("_cate_order")]
                #  合并两个列 保留存在 df_cate_tree里的
                select_cols = [c for c in cols_list + order_cols if c in self.df_cate_tree.columns]
                df_first_tree2 = self.df_cate_tree.select(*select_cols)
                if order_cols:
                    available_order_cols = [c for c in order_cols if c in df_first_tree2.columns]
                    if available_order_cols:
                        window_spec = Window.partitionBy(*cols_list).orderBy(
                            *[F.col(c).asc_nulls_last() for c in available_order_cols] # 按照order顺序依次排序
                        )
                        df_first_tree2 = df_first_tree2.withColumn("_rn", F.row_number().over(window_spec))
                        df_first_tree2 = df_first_tree2.filter(F.col("_rn") == 1).drop("_rn")
                else:
                    df_first_tree2 = df_first_tree2.dropDuplicates(cols_list)
                # 删除df_first_tree2的_cate_order_x 的序列
                for oc in order_cols:
                    if oc in df_first_tree2.columns:
                        df_first_tree2 = df_first_tree2.drop(oc)

                df_first_tree2 = df_first_tree2.withColumn("asin_bs_cate_parent_id_join", F.concat_ws("&&&&", *[
                    df_first_tree2[c].cast("string") for c in cols_list[:-1]]))
                df_node_num = df_node_num.join(df_first_tree2, how="left",
                                               on=["asin_bs_cate_1_id", f"asin_bs_cate_{num - 1}_id"])
            df_node_num_list2.append(df_node_num)

        df_nodes_num_concat2 = reduce(lambda a, b: a.unionByName(b, allowMissingColumns=True), df_node_num_list2)
        # ============================================================
        # 按 asin 去重，每个 asin 只保留一条路径 —— 保证"一个 asin 只算一次、不在祖先层双重计数"。
        # 【为什么排序键要加 parent_id_join】分类树是 DAG：同一个"最深分类"在同一一级下可经多个中间父级到达
        #   （us_bs_category 里有 3243 个 category_id 挂了多个父级）。上面 tree join 只用 (一级, 最深层) 两列作键，
        #   会把这种 asin 扇成多行、各带不同 parent_id_join；这些行来自同一个 num → _loop_order 相同。
        #   只按 _loop_order 排序时它们是"平局"，row_number 由 shuffle 顺序随机留一条 → 每次跑留下的中间路径不同
        #   → 中间层 group 集合忽有忽无 → 落表行数每次不一样（实测同代码两次跑 18912 / 18898）。
        # 数据本身不带中间路径、判不出"正确"父级，这里补 parent_id_join 升序做【确定性 tie-break】：
        #   平局时永远留字典序最小的那条路径 → 每次跑结果一致、可复现（只求稳定，不改"一个 asin 只算一次"的口径）。
        # 例：Cables(C) 同挂在 P1、P2 下 → 扇出 (E,P1,C)/(E,P2,C) 两行，parent_id_join 为 "E&&&&P1"/"E&&&&P2"
        #     → 升序永远留 "E&&&&P1" 那条。（一级等无扇出的 asin 只有一行，此键为 null、不影响结果）
        # ============================================================
        window_spec = Window.partitionBy("asin").orderBy(
            F.col("_loop_order").asc(),
            F.col("asin_bs_cate_parent_id_join").asc_nulls_last(),  # 确定性 tie-break：平局留字典序最小路径
        )
        # 生成行号并过滤
        self.df_nodes_num_concat2 = df_nodes_num_concat2.withColumn("rn", F.row_number().over(window_spec)) \
            .filter(F.col("rn") == 1) \
            .drop("rn")
        # 原缓存位置在此（clean_column 循环之前），循环会重新赋值成新 DF 使缓存失效 → 挪到循环之后
        # self.df_nodes_num_concat2.cache()
        #  处理 asin_bs_cate_ 空值 null
        id_cols = [c for c in self.df_nodes_num_concat2.columns if 'asin_bs_cate_' in c and '_id' in c]
        for id_col in id_cols:
            self.df_nodes_num_concat2 = self.clean_column(self.df_nodes_num_concat2, id_col)
        # 缓存最终版：handle_bs_level 每层 filter + 3 组集中度都读它
        self.df_nodes_num_concat2.cache()
        print("df_nodes_num_concat2打印")
        # self.df_nodes_num_concat2.show(10,truncate=False)
        # print(self.df_nodes_num_concat2.count())
        # print(self.df_nodes_num_concat2.columns)
    def _cr10_top_sum(self, df, group_keys, sales_col, out_col, salt_n=50):
        """
        两段式加盐取每个分类的 top10 销量之和（集中度分子），解决大分类窗口排序倾斜。
        原理：全局 top10 必然落在它所属盐桶的 top10 里 → 先按(分类,盐桶)各取 top10 候选，
        再从候选(每分类 ≤10*salt_n 行)取真正 top10 求和，结果与直接全量 top10 完全等价，
        但把"一个 task 排几百万行"拆成"salt_n 个 task 各排几万行"，消除 level=1 大类的倾斜。
        """
        df_s = df.withColumn("_salt", (F.rand() * salt_n).cast("int"))
        # 第一段：每个(分类,盐桶)内取 top10 候选
        w1 = Window.partitionBy(*group_keys, "_salt").orderBy(F.col(sales_col).desc_nulls_last())
        cand = df_s.withColumn("_rn_c", F.row_number().over(w1)).filter(F.col("_rn_c") <= 10)
        # 第二段：候选里取真正 top10 求和
        w2 = Window.partitionBy(*group_keys).orderBy(F.col(sales_col).desc_nulls_last())
        top = cand.withColumn("_rn_c2", F.row_number().over(w2)).filter(F.col("_rn_c2") <= 10)
        return top.groupBy(*group_keys).agg(F.sum(sales_col).alias(out_col))

    def handle_bs_level(self):
        print("handle_bs_level 调用")
        import re
        levels = []
        for col in self.df_nodes_num_concat2.columns:
            match = re.match(r'asin_bs_cate_(\d+)_id', col)
            if match:
                levels.append(int(match.group(1)))
        levels = sorted(set(levels)) #获取要遍历的asin_bs_cate_列表
        print("asin_bs_cate_列表",levels)
        # 获取所有 rate 列
        rate_cols = [c for c in self.df_nodes_num_concat2.columns if 'rate' in c]
        df_level_list = []
        #  10月数据八个层级
        # for level in range(1, 9):
        for level in levels:
            curr_col = f"asin_bs_cate_{level}_id"
            parent_col = f"asin_bs_cate_{level - 1}_id"
            filter_cond = F.col(curr_col).isNotNull()
            if level > 1 and parent_col in self.df_nodes_num_concat2.columns:
                filter_cond = filter_cond & F.col(parent_col).isNotNull()

            df_level = self.df_nodes_num_concat2.filter(filter_cond)
            if level == 1:
                group_keys = [curr_col]
            else:
                # ============================================================
                # 【多归属拆行 · 用当前层的祖先列分组】
                # 同一分类可能挂在多个父级/一级下（如 Cables 同时在 electronics、pc 下）。
                #   只按(当前,直接父级)分组会把不同一级来的 ASIN 合成一行、一级归属靠 min 随机挑、数量两支相加（超额）。
                # 【为什么不用 concat2 里现成的 asin_bs_cate_parent_id_join】那个是每个 ASIN "最深路径"的拼接，
                #   在中间层不代表"到当前层的路径"——用它当分组键会把某节点按各 ASIN 的更深路径过度打散
                #   （level=2 的行却带 level=6 的面包屑、裂成上百行）。
                # 用"一级..当前层"这几列祖先列现场分组：asin_bs_cate_1_id..asin_bs_cate_{level}_id，
                #   这才是"到当前层为止"的完整路径，唯一标识"这条路径下的这个节点"，多归属才按真实路径裂行。
                # 【例】三级 X：(electronics,Cables,X)、(pc,Cables,X) 各一行，各算各的。
                # ============================================================
                group_keys = [f"asin_bs_cate_{i}_id" for i in range(1, level + 1)]

            aggs = [
                F.sum("asin_amazon_orders").alias("asin_amazon_orders"),
                F.sum("asin_bsr_orders").alias("asin_bsr_orders"),
                F.sum("asin_bsr_orders_new").alias("asin_bsr_orders_new"),
                F.sum("asin_bsr_orders_brand").alias("asin_bsr_orders_brand"),
                F.sum("asin_bsr_orders_sales").alias("asin_bsr_orders_sales"),
                F.sum("asin_bsr_orders_sales_new").alias("asin_bsr_orders_sales_new"),
                F.sum("asin_bsr_orders_sales_brand").alias("asin_bsr_orders_sales_brand"),
                F.sum("asin_is_new").alias("asin_is_new"),  # 已转int
                F.sum("is_brand_label").alias("is_brand_label"),  # 已转int
                F.count("asin").alias("asin"),
                F.avg("asin_price").alias("asin_price"),
                F.avg("asin_rating").alias("asin_rating"),
                F.avg("asin_bs_cate_1_rank").alias("asin_bs_cate_1_rank"),
                F.avg("asin_total_comments").alias("asin_total_comments"),
                # 类目下海运/空运平均利润率 利润率为空的产品不计入
                # 只对有利润率数据的 ASIN 求平均。整组全空 → avg 返回 null
                F.avg("asin_ocean_freight_gross_margin").alias("asin_ocean_freight_gross_margin_avg"),
                F.avg("asin_air_freight_gross_margin").alias("asin_air_freight_gross_margin_avg"),
            ]
            # 区间利润率(价格+月销)：对 24 个带闸列各求 F.avg = 该分类各区间段的平均利润率(段外行是 null 自动跳过)。
            # 别名保持与带闸列同名(不加 _avg)，避免多套命名；下游 round/落表按这批列名统一处理。
            for pbc in self.band_margin_cols:
                aggs.append(F.avg(F.col(pbc)).alias(pbc))
            # 打包段)：对 7 个带闸月销列各求 F.sum = 该分类该打包段的月销合计(段外 null 不计)。
            # 这里只求和；占比(÷品类总月销)在 calculate_column 里算。别名同名。
            for pkc in self.pkg_band_cols:
                aggs.append(F.sum(F.col(pkc)).alias(pkc))

            #添加 rate 列的聚合
            for rc in rate_cols:
                # aggs.append(F.sum(F.col(rc)).alias(rc))
                aggs.append(F.sum(F.col(rc).cast("int")).alias(rc))
            # 注：parent_id_join 已作为分组键（见上方），不再用 F.min 聚合——每组本就只有一个路径值

            df_res = df_level.groupBy(group_keys).agg(*aggs)

            # ============================================================
            # 【集中度 CR10】：每个分类算 top10 实体销量 ÷ 品类总月销。
            # 前面都是"整体聚合"(sum/avg)，集中度要"分类内排序取前10再求和"，是窗口排序，
            #   必须在按 group_keys(当前层完整路径)分好组后、rename 之前，用 Window.partitionBy(group_keys) 做。
            # 【月销要 cast double】asin_amazon_orders 是 string，orderBy 不转会按字典序("9">"1000")排错；
            #   sum 也转 double，与分母 asin_bsr_orders_sum(Spark 对 string 求 sum 亦转 double)口径一致。
            # 【null 处理】月销 null → desc_nulls_last 排最后、不进 top10，sum 也跳过；
            #   品牌/卖家 null → 不是一个真实品牌/卖家，先 filter 掉再排名(它们的销量仍在分母品类总月销里，
            #   故品牌/卖家集中度可能 <1，口径=A：分母统一品类总月销，见设计确认)。
            # 【不足10个】全取 → 商品集中度=1、品牌/卖家≤1，正常。
            # 三个 top10 求和列 _c_asin/_c_brand/_c_seller 先 join 回 df_res，占比在 calculate_column 里 ÷ 总月销。
            # ============================================================
            orders_d = F.col("asin_amazon_orders").cast("double")
            # ===== 集中度 CR10：单窗口全量排序取 top10（正式版）。曾试两段式加盐版(见下方注释)消除 level=1 大类倾斜，
            #   但实测加盐前后总耗时基本一致(~1h40m，瓶颈在拆层+8层 groupBy 不在这一步)，故保留更简单的单窗口版。 =====
            # 商品集中度：asin 直接按月销排序取 top10
            w_asin = Window.partitionBy(*group_keys).orderBy(orders_d.desc_nulls_last())
            df_c_asin = df_level.withColumn("_rn_c", F.row_number().over(w_asin)) \
                .filter(F.col("_rn_c") <= 10) \
                .groupBy(group_keys).agg(F.sum(orders_d).alias("_c_asin_sales"))
            df_res = df_res.join(df_c_asin, on=group_keys, how="left")
            # 品牌集中度：先按(路径,品牌)汇总月销(排除无品牌)，再排序取 top10 品牌求和
            df_brand_sales = df_level.filter(F.col("asin_brand_name").isNotNull() & (F.col("asin_brand_name") != "")) \
                .groupBy(*group_keys, "asin_brand_name").agg(F.sum(orders_d).alias("_brand_sales"))
            w_brand = Window.partitionBy(*group_keys).orderBy(F.col("_brand_sales").desc_nulls_last())
            df_c_brand = df_brand_sales.withColumn("_rn_c", F.row_number().over(w_brand)) \
                .filter(F.col("_rn_c") <= 10) \
                .groupBy(group_keys).agg(F.sum("_brand_sales").alias("_c_brand_sales"))
            df_res = df_res.join(df_c_brand, on=group_keys, how="left")
            # 卖家集中度：同品牌，把 asin_brand_name 换成 account_id(排除无卖家)
            df_seller_sales = df_level.filter(F.col("account_id").isNotNull() & (F.col("account_id") != "")) \
                .groupBy(*group_keys, "account_id").agg(F.sum(orders_d).alias("_seller_sales"))
            w_seller = Window.partitionBy(*group_keys).orderBy(F.col("_seller_sales").desc_nulls_last())
            df_c_seller = df_seller_sales.withColumn("_rn_c", F.row_number().over(w_seller)) \
                .filter(F.col("_rn_c") <= 10) \
                .groupBy(group_keys).agg(F.sum("_seller_sales").alias("_c_seller_sales"))
            df_res = df_res.join(df_c_seller, on=group_keys, how="left")

            # ===== 加盐版(两段式取 top10，结果与单窗口等价、消除大类倾斜)保留备查；耗时无优势故不启用 =====
            # df_level_o = df_level.withColumn("_orders_d", orders_d)
            # df_c_asin = self._cr10_top_sum(df_level_o, group_keys, "_orders_d", "_c_asin_sales")
            # df_res = df_res.join(df_c_asin, on=group_keys, how="left")
            # df_brand_sales = df_level_o.filter(F.col("asin_brand_name").isNotNull() & (F.col("asin_brand_name") != "")) \
            #     .groupBy(*group_keys, "asin_brand_name").agg(F.sum("_orders_d").alias("_brand_sales"))
            # df_c_brand = self._cr10_top_sum(df_brand_sales, group_keys, "_brand_sales", "_c_brand_sales")
            # df_res = df_res.join(df_c_brand, on=group_keys, how="left")
            # df_seller_sales = df_level_o.filter(F.col("account_id").isNotNull() & (F.col("account_id") != "")) \
            #     .groupBy(*group_keys, "account_id").agg(F.sum("_orders_d").alias("_seller_sales"))
            # df_c_seller = self._cr10_top_sum(df_seller_sales, group_keys, "_seller_sales", "_c_seller_sales")
            # df_res = df_res.join(df_c_seller, on=group_keys, how="left")

            # 换 node 后输出列名从 asin_bs_cate_* 改为 asin_node_*（值是页面 node id，不再是 BSR 分类 id）。
            #   注意：内部级列 asin_bs_cate_{i}_id 仍沿用旧名（只是中间分组列，不落表），故这里只改输出的 3 个列名。
            df_res = df_res.withColumnRenamed(curr_col, "asin_node_current_id")
            if level == 1:
                df_res = df_res.withColumn("asin_node_parent_id", F.lit("0"))
                df_res = df_res.withColumn("asin_node_parent_id_join", F.lit("0"))
            else:
                # 面包屑 = 0&&&&一级&&&&…&&&&直接父级，用"一级..上一层"祖先列现场拼。
                # 必须在 rename parent_col 之前拼（此时 asin_bs_cate_{level-1}_id 还在），拼完再 rename。
                ancestor_join_cols = [f"asin_bs_cate_{i}_id" for i in range(1, level)]  # 1..level-1
                df_res = df_res.withColumn(
                    "asin_node_parent_id_join",
                    F.concat(F.lit("0&&&&"),
                             F.concat_ws("&&&&", *[F.col(c).cast("string") for c in ancestor_join_cols]))
                )
                df_res = df_res.withColumnRenamed(parent_col, "asin_node_parent_id")
                # 删掉分组用剩的中间祖先列（1..level-2），只保留 current_id/parent_id/parent_id_join，
                # 否则它们会随 unionByName 混进最终结果，污染列结构。
                for _c in [f"asin_bs_cate_{i}_id" for i in range(1, level - 1)]:
                    if _c in df_res.columns:
                        df_res = df_res.drop(_c)

            df_res = df_res.withColumn("level", F.lit(level))

            # 打印行数进行对比
            # print(f"Level {level} Count: {df_res.count()}")
            df_level_list.append(df_res)
        self.df_level_concat = reduce(lambda a, b: a.unionByName(b, allowMissingColumns=True), df_level_list)
        # join 后即被重新赋值、从未被用的死缓存，去掉
        # self.df_level_concat.cache()
        # print("self.df_level_concat",self.df_level_concat.count())
        # print(self.df_level_concat.columns)
        #     增加分类名称
        print("增加分类名称（keepa_category.name）")
        # ============================================================
        # 换 node 后分类名从 keepa_category 取：当前层 id(asin_node_current_id)=keepa cat_id → name。
        # 【旧逻辑】原从 BSR 树表 df_cate_bs_data 取 cur_category_name，inner join（会丢 2 个非标准一级）。整段替换。
        # 【为什么改 inner 为 left】现在 current_id 都来自 keepa 树，正常都能在 keepa_category 查到名；
        #   用 left 兜底：万一个别 id 查不到名也不丢整条分类行（cur_category_name 是废弃展示字段、后端不查，
        #   为空无害；但分类行本身的各项统计不能因缺名被丢）。
        # ============================================================
        df_cate_name = self.df_cate_name.select(
            F.col("cat_id").alias("asin_node_current_id"),
            F.col("name").alias("cur_category_name")
        ).dropDuplicates(["asin_node_current_id"])
        self.df_level_concat = self.df_level_concat.join(df_cate_name, on='asin_node_current_id', how="left")

        # ============================================================
        # 增加 leaf_node（是否叶子分类）：直接取 keepa_category_path.leaf_node（1=叶子/最细分类，0=还有子分类）。
        # 【用途】前端"默认显示最小节点(叶子)分类"时按 leaf_node=1 筛。
        # 【为什么 left + coalesce 0】current_id 都来自 keepa 树、正常都能匹配上；万一没匹配到（理论不会）
        #   补 0（当非叶子），避免落 null（Java 侧读 NULL 有问题，见项目约定）。
        # ============================================================
        df_leaf = self.df_path.select(
            F.col("cat_id").alias("asin_node_current_id"),
            F.col("leaf_node")
        ).dropDuplicates(["asin_node_current_id"])
        self.df_level_concat = self.df_level_concat.join(df_leaf, on='asin_node_current_id', how="left")
        self.df_level_concat = self.df_level_concat.withColumn(
            "leaf_node", F.coalesce(F.col("leaf_node").cast("int"), F.lit(0))
        )
        print("增加分类名称 join后的数据")
        # self.df_level_concat.show(10,truncate=False )
        # print(self.df_level_concat.count())
        # print(self.df_level_concat.columns)
        # print("df_cate_bs_data的_cate_order的值是")
        # self.df_cate_bs_data.select("_cate_order").show()
    def handle_bs_level_calculate_column(self):
        print("handle_bs_level_calculate_column 调用 字段占比计算")
        rename_columns = {
            "asin_amazon_orders": "asin_amazon_orders_sum",
            # asin_bsr_orders 底层已在 handle_bs_add_column 重定向为前台月销 →
            # asin_bsr_orders_sum 实为"前台月销总销量"，数值等同 asin_amazon_orders_sum，页面不显示此列
            "asin_bsr_orders": "asin_bsr_orders_sum",
            "asin_bsr_orders_new": "asin_bsr_orders_sum_new",
            "asin_bsr_orders_brand": "asin_bsr_orders_sum_brand",
            "asin_bsr_orders_sales": "asin_bsr_orders_sales_sum",
            "asin_bsr_orders_sales_new": "asin_bsr_orders_sales_sum_new",
            "asin_bsr_orders_sales_brand": "asin_bsr_orders_sales_sum_brand",
            "asin_is_new": "asin_count_new",
            "is_brand_label": "asin_count_brand",
            "asin": "asin_count",
            "asin_price": "asin_price_mean",
            "asin_rating": "asin_rating_mean",
            "asin_bs_cate_1_rank": "asin_bs_cate_1_rank_mean",
            "asin_total_comments": "asin_total_comments_mean"
        }
        for old_col, new_col in rename_columns.items():
            self.df_level_concat = self.df_level_concat.withColumnRenamed(old_col, new_col)

            # 新品占比
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_count_rate_new",
            F.col("asin_count_new") / F.col("asin_count")
        )
        # 分母=当前分类总销量，为 0/null（该分类无销量）→ -1 占位（无法计算）
        # 分子=新品销量 sum，null 表示无可加行 → coalesce 按真实 0 计
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_rate_new",
            F.when(F.col("asin_bsr_orders_sum").isNull() | (F.col("asin_bsr_orders_sum") == 0), F.lit(-1))
             .otherwise(F.coalesce(F.col("asin_bsr_orders_sum_new"), F.lit(0)) / F.col("asin_bsr_orders_sum"))
        )

        # 品牌占比
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_count_rate_brand",
            F.col("asin_count_brand") / F.col("asin_count")
        )
        # 分母=当前分类总销量，为 0/null → -1 占位；分子=品牌销量 sum，null 按真实 0 计
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_rate_brand",
            F.when(F.col("asin_bsr_orders_sum").isNull() | (F.col("asin_bsr_orders_sum") == 0), F.lit(-1))
             .otherwise(F.coalesce(F.col("asin_bsr_orders_sum_brand"), F.lit(0)) / F.col("asin_bsr_orders_sum"))
        )

        # 平均销售额
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_mean",
            F.col("asin_bsr_orders_sales_sum") / F.col("asin_count")
        )
        # 分母=新品数，为 0/null（无新品）→ -1 占位；分子=新品销售额 sum，null 按真实 0 计
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_mean_new",
            F.when(F.col("asin_count_new").isNull() | (F.col("asin_count_new") == 0), F.lit(-1))
             .otherwise(F.coalesce(F.col("asin_bsr_orders_sales_sum_new"), F.lit(0)) / F.col("asin_count_new"))
        )
        # 分母=品牌数，为 0/null（无品牌）→ -1 占位；分子=品牌销售额 sum，null 按真实 0 计
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_mean_brand",
            F.when(F.col("asin_count_brand").isNull() | (F.col("asin_count_brand") == 0), F.lit(-1))
             .otherwise(F.coalesce(F.col("asin_bsr_orders_sales_sum_brand"), F.lit(0)) / F.col("asin_count_brand"))
        )

        # 平均月销量
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_mean",
            F.col("asin_bsr_orders_sum") / F.col("asin_count")
        )
        # 分母=新品数，为 0/null（无新品）→ -1 占位；分子=新品销量 sum，null 按真实 0 计
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_mean_new",
            F.when(F.col("asin_count_new").isNull() | (F.col("asin_count_new") == 0), F.lit(-1))
             .otherwise(F.coalesce(F.col("asin_bsr_orders_sum_new"), F.lit(0)) / F.col("asin_count_new"))
        )
        # 分母=品牌数，为 0/null（无品牌）→ -1 占位；分子=品牌销量 sum，null 按真实 0 计
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_mean_brand",
            F.when(F.col("asin_count_brand").isNull() | (F.col("asin_count_brand") == 0), F.lit(-1))
             .otherwise(F.coalesce(F.col("asin_bsr_orders_sum_brand"), F.lit(0)) / F.col("asin_count_brand"))
        )

        # ========== 3. 保留小数位数 ==========
        # round(0) 保留整数
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum", F.round("asin_bsr_orders_sum", 0)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_new", F.round("asin_bsr_orders_sum_new", 0)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_brand", F.round("asin_bsr_orders_sum_brand", 0)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_count_new", F.round("asin_count_new", 0)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_count_brand", F.round("asin_count_brand", 0)
        )
        # 月均销量属"件数"→取整（2026-07-02 补 round(0)，统一口径：销量=整数）。
        # 这三列上面是除法(sum/count)得 double，之前漏了取整会带小数；mean_new/mean_brand 的 -1 占位 round 后仍是 -1，不受影响。
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_mean", F.round("asin_bsr_orders_mean", 0)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_mean_new", F.round("asin_bsr_orders_mean_new", 0)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_mean_brand", F.round("asin_bsr_orders_mean_brand", 0)
        )
        # 均值列 F.avg 遇"整组该字段全 null"（如某冷门分类下所有 asin 都无排名）会返回 NULL；
        # 按项目约定 Java 不存 NULL → coalesce 填 -1 占位（排名/价格/评分均无负数，-1 不会和真实值混淆，Java 转 null）。
        # 已实测该分区 rank/price/rating 均存在整组全空的分类（comments 恒非空故不守）。
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bs_cate_1_rank_mean", F.coalesce(F.round("asin_bs_cate_1_rank_mean", 0), F.lit(-1))
        )
        # comments 当前上游 fillna(0) 恒非空，avg 不会 NULL；仍一并兜底，防上游哪天去掉 fillna（-1 占位同上）
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_total_comments_mean", F.coalesce(F.round("asin_total_comments_mean", 0), F.lit(-1))
        )

        # round(2) 保留2位小数
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_sum", F.round("asin_bsr_orders_sales_sum", 2)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_mean", F.round("asin_bsr_orders_sales_mean", 2)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_sum_new", F.round("asin_bsr_orders_sales_sum_new", 2)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_mean_new", F.round("asin_bsr_orders_sales_mean_new", 2)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_sum_brand", F.round("asin_bsr_orders_sales_sum_brand", 2)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sales_mean_brand", F.round("asin_bsr_orders_sales_mean_brand", 2)
        )
        # 同上：价格/评分均值整组全空 → NULL，coalesce 填 -1 占位
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_price_mean", F.coalesce(F.round("asin_price_mean", 2), F.lit(-1))
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_rating_mean", F.coalesce(F.round("asin_rating_mean", 2), F.lit(-1))
        )

        # round(4) 保留4位小数
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_count_rate_new", F.round("asin_count_rate_new", 4)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_count_rate_brand", F.round("asin_count_rate_brand", 4)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_rate_new", F.round("asin_bsr_orders_sum_rate_new", 4)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_rate_brand", F.round("asin_bsr_orders_sum_rate_brand", 4)
        )
        # 海运/空运平均利润率：比例，round(4)。整组全空(该分类下无任一 ASIN 有利润率) → avg 返回 null。
        # 【和其它均值列不同】此列无值时直接落 null（不填 -1/-1000 占位）——已与后端确认利润率列可收 null。
        # 好处：null 天然不参与 同比环比的算术；坏处/注意：落表 save_data 时这批利润率列不能 na.fill。
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_ocean_freight_gross_margin_avg",
            F.round("asin_ocean_freight_gross_margin_avg", 4)
        )
        self.df_level_concat = self.df_level_concat.withColumn(
            "asin_air_freight_gross_margin_avg",
            F.round("asin_air_freight_gross_margin_avg", 4)
        )
        # 区间利润率 24 列(价格12+月销12)：同利润率均值，比例 round(4)，整段全空 → avg 为 null → 直接落 null（同上）。
        for pbc in self.band_margin_cols:
            self.df_level_concat = self.df_level_concat.withColumn(
                pbc, F.round(pbc, 4)
            )

        # 打包段月销占比 7 列：段月销合计 ÷ 品类总月销(asin_bsr_orders_sum) = 该段占比。
        # 分母=品类总月销，为 0/null(该分类无月销) → -1 占位(无法计算，与其它占比列口径一致，Java 转 null)。
        # 分子=段月销 sum，null(该段无月销 asin) → coalesce 按真实 0 计。生成 _rate 后缀的最终占比列，round(4)。
        # 注：占比列名含 "rate" 无妨——rate_cols 在 handle_bs_level 已用完，此处是更后面的 calculate_column。
        for pkc in self.pkg_band_cols:
            rate_col = pkc.replace("_orders_sum", "_orders_rate")
            self.df_level_concat = self.df_level_concat.withColumn(
                rate_col,
                F.when(F.col("asin_bsr_orders_sum").isNull() | (F.col("asin_bsr_orders_sum") == 0), F.lit(-1))
                 .otherwise(F.round(F.coalesce(F.col(pkc), F.lit(0)) / F.col("asin_bsr_orders_sum"), 4))
            )

        # 集中度 CR10 3 列：top10 实体销量(_c_*_sales，handle_bs_level 里算好) ÷ 品类总月销(asin_bsr_orders_sum)。
        # 分母=品类总月销，为 0/null → -1 占位(同其它占比列口径)。分子 null(该分类无对应实体) → coalesce 0。round(4)。
        # 商品集中度分母含全部月销、分子是 top10 asin → 通常接近满值；品牌/卖家分子排除了无品牌/无卖家 → 可能 <1(口径A)。
        for raw_col, out_col in [
            ("_c_asin_sales",   "asin_concentration"),
            ("_c_brand_sales",  "brand_concentration"),
            ("_c_seller_sales", "seller_concentration"),
        ]:
            self.df_level_concat = self.df_level_concat.withColumn(
                out_col,
                F.when(F.col("asin_bsr_orders_sum").isNull() | (F.col("asin_bsr_orders_sum") == 0), F.lit(-1))
                 .otherwise(F.round(F.coalesce(F.col(raw_col), F.lit(0)) / F.col("asin_bsr_orders_sum"), 4))
            )

    # 注：handle_bs_level_proportion（非2版，2026-07-02 删除）——groupBy+inner join 逐层直译版，慢且 inner join 可能丢行，
    #     由下方 handle_bs_level_proportion2（窗口函数一次算出父级总销量，等价且更快、不丢行）取代。
    #     如需找回：开发母本 spark_hjm/bs_top100/dwt_bs_top100.py 或 git 历史。

    def handle_bs_level_proportion2(self):
        print("handle_bs_level_proportion 调用 计算层级占比")
        # 下游到写出是线性链只读一次，去掉
        # self.df_level_concat.cache()
        # 2. 定义窗口：按"完整父级路径 parent_id_join"分组（不是只按直接父级 id）
        #   直接父级 id 在多归属时不唯一——如 Cables 同时挂在 electronics、pc 下，只按 Cables 分组会把
        #   两个一级支的子级销量全加进分母 → 占比偏小、跨一级混算（分母 220、实际同支应为 160）。
        #   parent_id_join 形如 "0&&&&electronics&&&&Cables"，唯一标识"某条路径下的这个父级"，只有同路径
        #   的兄弟才进同一分母。level=1(值"0")、level=2(值"0&&&&一级")的分组与按直接父级 id 完全等价，
        #   只有 level≥3 的多归属分类被修正——和 handle_bs_level 的拆行改动对齐。
        window_spec = Window.partitionBy("asin_node_parent_id_join")
        # 3. 计算父节点总订单数
        # 这一步相当于原来的: agg(sum("asin_bsr_orders_sum")) + join
        # 效果：每一行都会多出一列 'asin_bsr_orders_sum_parent'，
        #      这一列的值等于该行所属父分类下所有行的 orders_sum 之和。
        # 父级总销量也属"件数"→取整（2026-07-02 补 round(0)，统一口径：销量=整数）。
        # 它是占比分母(本级/父级)，取整后与分子 asin_bsr_orders_sum 同为整数，口径一致。
        self.df_save = self.df_level_concat.withColumn(
            "asin_bsr_orders_sum_parent",
            F.round(F.sum("asin_bsr_orders_sum").over(window_spec), 0)
        )
        # 下游到写出是线性链只读一次，去掉
        # self.df_save.cache()
        # 4. 计算占比
        # 逻辑：自身订单 / 父节点总订单
        # 分母=父级总销量，为 0/null（整支无销量）→ -1 占位（无法计算，Java 转 null），与其余占比列口径统一
        self.df_save = self.df_save.withColumn(
            "asin_bsr_orders_sum_rate",
            F.when(F.col("asin_bsr_orders_sum_parent") != 0,
                   F.round(F.col("asin_bsr_orders_sum") / F.col("asin_bsr_orders_sum_parent"), 4)
                   ).otherwise(F.lit(-1))
        )

        # 释放缓存
        # 上面已不缓存 df_level_concat，unpersist 无对象，去掉
        # self.df_level_concat.unpersist()


    def handle_bs_proportion(self):
        print("handle_bs_proportion调用")
        # 占比计算
        # 定义需要处理的列名列表
        rate_cols = [
            "launch_time_type0_rate", "launch_time_type1_rate", "launch_time_type2_rate",
            "launch_time_type3_rate", "launch_time_type4_rate", "launch_time_type5_rate",
            "launch_time_type6_rate", "launch_time_type7_rate",
            "asin_buy_box_seller_type0_rate", "asin_buy_box_seller_type1_rate",
            "asin_buy_box_seller_type2_rate", "asin_buy_box_seller_type3_rate",
            "asin_buy_box_seller_type4_rate",
            # "其他"档(=type0+type4)：与上面五列同为数量占比，同样 ÷ asin_count。与 type1/2/3 互斥穷尽、四者相加=1。
            "asin_buy_box_seller_type_other_rate",
            #  卖家所属地 3 列：与上架时间/卖家类型同为"数量占比"，同样 ÷ asin_count。
            # 三列 null-safe 互斥穷尽(见 handle_bs_categorical_data)，计数和=asin_count → 三占比相加=1。
            "seller_country_us_rate", "seller_country_cn_rate", "seller_country_other_rate",
            # 月销50+/100+/200+ 3 列：达标 asin 数 ÷ asin_count。累计阈值不互斥、不相加=1。
            "asin_orders_ge_50_rate", "asin_orders_ge_100_rate", "asin_orders_ge_200_rate"
        ]

        # 使用循环批量更新列：桶计数 ÷ 该分类总数量 = 占比，round(4) 对齐目标表 double(10,4)
        # 分母 asin_count 恒 ≥1、分子(桶计数 sum)上游无 null 恒非空 → 除法不会 null，只需取整，无需 -1 守卫
        for col_name in rate_cols:
            self.df_save = self.df_save.withColumn(col_name, F.round(F.col(col_name) / F.col("asin_count"), 4))

    def handle_data(self):
        # ============================================================
        # 归属换 node_id 后的编排：
        #   旧的两步（BSR 树去重 + 自拼树）不再需要，注释保留；
        #   asin_deduplicated（BSR 树 join 取最深）换成 node_attach（挂 node_id + node_id_path、空 node 排除）；
        #   nodes_num_concat2（按深度分桶 + tree join 回补）换成 node_path_to_row（拆 node_id_path 为各级 id 列）。
        #   handle_bs_level 及其后全不动。
        # ============================================================
        # self.handle_bs_redirect_flag()     # 旧：BSR 树去重，换 node 后无树，停用（方法保留备查）
        # self.handle_bs_hierarchy_to_row()  # 旧：自拼 BSR 层级树，换 node_id_path 后不需要，停用（方法保留备查）
        self.handle_bse_column_rename()
        self.handle_bs_categorical_data()
        # self.handle_bs_asin_deduplicated() # 旧：BSR (一级,current) join us_bs_category 取最深，停用（方法保留备查）
        self.handle_bs_node_attach()         # 新：挂 node_id + node_id_path，空 node 排除
        self.handle_bs_add_column()
        # self.handle_bs_nodes_num_concat2() # 旧：按 nodes_num 分桶 + tree join 回补祖先，停用（方法保留备查）
        self.handle_bs_node_path_to_row()    # 新：拆 node_id_path → asin_bs_cate_1_id..N_id
        self.handle_bs_level()
        self.handle_bs_level_calculate_column()
        self.handle_bs_level_proportion2()
        self.handle_bs_proportion()

    def save_data(self):
        print("save_data 调用：落 Hive dwt_bs_top100")

        # ============================================================
        # 44 业务列 + 利润率均值2 + 区间利润率24(self.band_margin_cols)
        #   + 打包占比7 + 卖家地占比3 + 月销阈值占比3 + 集中度3 + 卖家类型其他档1 = 88 列，再 + 3 分区列。
        #   （原注释写 44 业务列/86 列，实际 business_cols 是 45 个，已按脚本核对修正）
        # ============================================================
        business_cols = [
            "asin_node_current_id",
            "asin_amazon_orders_sum", "asin_bsr_orders_sum", "asin_bsr_orders_sum_new", "asin_bsr_orders_sum_brand",
            "asin_bsr_orders_sales_sum", "asin_bsr_orders_sales_sum_new", "asin_bsr_orders_sales_sum_brand",
            "asin_count_new", "asin_count_brand", "asin_count",
            "asin_price_mean", "asin_rating_mean", "asin_bs_cate_1_rank_mean", "asin_total_comments_mean",
            "launch_time_type0_rate", "launch_time_type1_rate", "launch_time_type2_rate", "launch_time_type3_rate",
            "launch_time_type4_rate", "launch_time_type5_rate", "launch_time_type6_rate", "launch_time_type7_rate",
            "asin_buy_box_seller_type0_rate", "asin_buy_box_seller_type1_rate", "asin_buy_box_seller_type2_rate",
            "asin_buy_box_seller_type3_rate", "asin_buy_box_seller_type4_rate",
            "asin_node_parent_id", "asin_node_parent_id_join", "level", "cur_category_name", "leaf_node",
            "asin_count_rate_new", "asin_bsr_orders_sum_rate_new", "asin_count_rate_brand", "asin_bsr_orders_sum_rate_brand",
            "asin_bsr_orders_sales_mean", "asin_bsr_orders_sales_mean_new", "asin_bsr_orders_sales_mean_brand",
            "asin_bsr_orders_mean", "asin_bsr_orders_mean_new", "asin_bsr_orders_mean_brand",
            "asin_bsr_orders_sum_parent", "asin_bsr_orders_sum_rate",
        ]
        profit_avg_cols = ["asin_ocean_freight_gross_margin_avg", "asin_air_freight_gross_margin_avg"]
        pkg_rate_cols = [c.replace("_orders_sum", "_orders_rate") for c in self.pkg_band_cols]
        country_cols = ["seller_country_us_rate", "seller_country_cn_rate", "seller_country_other_rate"]
        orders_ge_cols = ["asin_orders_ge_50_rate", "asin_orders_ge_100_rate", "asin_orders_ge_200_rate"]
        conc_cols = ["asin_concentration", "brand_concentration", "seller_concentration"]
        # 卖家类型"其他"档：放最末尾，与 Hive 表列顺序对齐。
        # saveAsTable 本身按列名映射、df 顺序无所谓；但这张表是 LazySimpleSerDe 文本格式，
        # 表定义的列顺序=数据文件里字段的物理顺序，新列插中间会让只有 86 字段的老分区整体错位。
        seller_type_other_cols = ["asin_buy_box_seller_type_other_rate"]
        final_cols = business_cols + profit_avg_cols + list(self.band_margin_cols) \
                     + pkg_rate_cols + country_cols + orders_ge_cols + conc_cols + seller_type_other_cols

        # ============================================================
        # 【销量/金额 sum 列】某分类全无月销时 sum=null → coalesce(0)（0=无销量，有意义，区别于利润率的"无数据"）。
        # 【利润率 26 列】保留 null（已与后端确认可收 null，不填占位）。
        # 【均值/占比/集中度列】计算时已填 -1 占位，不动。
        # 【计数/其它占比】分母 asin_count≥1、分子非空，不会 null，不处理。
        # ============================================================
        zero_fill = {
            "asin_amazon_orders_sum", "asin_bsr_orders_sum", "asin_bsr_orders_sum_new", "asin_bsr_orders_sum_brand",
            "asin_bsr_orders_sales_sum", "asin_bsr_orders_sales_sum_new", "asin_bsr_orders_sales_sum_brand",
            "asin_bsr_orders_mean", "asin_bsr_orders_sum_parent",
            # 补漏：这三列同为"分子可能整组 null"却之前未兜底，会给后端落 null（违反不存 null 铁律）：
            #   sales_mean = sales_sum/count，sales_sum 整组月销或价格全 null 时为 null（同 bsr_orders_mean 却漏填）；
            #   count_new = sum(asin_is_new)，asin_is_new 上游 dwt_flow_asin 未 na.fill、整组全 null 时为 null；
            #   count_rate_new = count_new/count 随之 null。（count_brand 侧 is_brand_label 上游填了 0，安全，无需补）
            "asin_bsr_orders_sales_mean", "asin_count_new", "asin_count_rate_new",
        }
        select_exprs = []
        for c in final_cols:
            if c in zero_fill:
                select_exprs.append(F.coalesce(F.col(c), F.lit(0)).alias(c))
            else:
                select_exprs.append(F.col(c))
        # 补 3 个分区列（放最后，与建表 PARTITIONED BY 一致）
        select_exprs += [
            F.lit(self.site_name).alias("site_name"),
            F.lit(self.date_type).alias("date_type"),
            F.lit(self.date_info).alias("date_info"),
        ]
        df_save = self.df_save.select(*select_exprs)


        partition_dict = {"site_name": self.site_name, "date_type": self.date_type, "date_info": self.date_info}
        hdfs_path = CommonUtil.build_hdfs_path(self.db_save, partition_dict=partition_dict)
        print(f"清除 hdfs 目录：{hdfs_path}")
        HdfsUtils.delete_file_in_folder(hdfs_path)

        print(f"落表 {self.db_save}，分区 {self.partitions_by}，date_info {date_info}")
        self.save_data_common(df_save=df_save, db_save=self.db_save,
                              partitions_num=self.partitions_num, partitions_by=self.partitions_by)
        # 落表后回查该分区实际条数（读已写分区，不重算 df_save 整条链路）
        written_count = self.spark.sql(
            f"select count(1) as cnt from {self.db_save} "
            f"where site_name='{self.site_name}' and date_type='{self.date_type}' and date_info='{self.date_info}'"
        ).collect()[0]["cnt"]
        print(f"save_data 成功，落表条数={written_count}")


if __name__ == '__main__':
    site_name = sys.argv[1]  # 参数1：站点  us
    date_type = sys.argv[2]  # 参数2：类型：week/4_week/month/quarter month
    date_info = sys.argv[3]  # 参数3：年-周/年-月/年-季, 比如: 2022-1 2025-11
    handle_obj = DwtBsTop100(site_name=site_name, date_type=date_type, date_info=date_info)
    handle_obj.run()
