|
@@ -9,34 +9,41 @@
|
|
|
|
|
|
|
|
报告结构(2026/08/14 由单 Sheet 分区改为多 Sheet,每 sheet 独立列宽、蓝条只覆盖本表宽度):
|
|
报告结构(2026/08/14 由单 Sheet 分区改为多 Sheet,每 sheet 独立列宽、蓝条只覆盖本表宽度):
|
|
|
Sheet 平台总览 :平台汇总 + 当日组齐环比(vs 昨日同窗口) + 商家 GMV 集中度(Top1/3/5/10) + 口径脚注
|
|
Sheet 平台总览 :平台汇总 + 当日组齐环比(vs 昨日同窗口) + 商家 GMV 集中度(Top1/3/5/10) + 口径脚注
|
|
|
- Sheet 产品系列榜:当日各系列 GMV 榜(Top,含占比)
|
|
|
|
|
|
|
+ Sheet 品类·系列榜:当日品类汇总(成团数/GMV/占比,品类由标题判定) + 各系列 GMV 榜(Top,含品类列与占比)
|
|
|
Sheet 商家GMV榜 :当日组齐 GMV 前 N 商家(含占比)
|
|
Sheet 商家GMV榜 :当日组齐 GMV 前 N 商家(含占比)
|
|
|
Sheet 运营节奏 :重点商家当日运营快照(新开团/已组齐/规格) + 平台组齐时段分布(近7日24h)
|
|
Sheet 运营节奏 :重点商家当日运营快照(新开团/已组齐/规格) + 平台组齐时段分布(近7日24h)
|
|
|
Sheet 魔都明细 :881226408 汇总 + 每条明细(含「参与人数(购买记录)」与售卖进度里程碑列;
|
|
Sheet 魔都明细 :881226408 汇总 + 每条明细(含「参与人数(购买记录)」与售卖进度里程碑列;
|
|
|
汇总下附「购买记录覆盖检测」= 成交团 vs 已采购买记录,标注漏采多少 T(团))
|
|
汇总下附「购买记录覆盖检测」= 成交团 vs 已采购买记录,标注漏采多少 T(团))
|
|
|
Sheet 用户排行榜(魔都):881226408 买家榜(deca_buy_record 按 user_id 聚合,参与金额倒序,
|
|
Sheet 用户排行榜(魔都):881226408 买家榜(deca_buy_record 按 user_id 聚合,参与金额倒序,
|
|
|
含 参与车数 / 参与金额 / 车均消费)(2026/08/17 新增)
|
|
含 参与车数 / 参与金额 / 车均消费)(2026/08/17 新增)
|
|
|
- Sheet 卡皇明细 :274584650 汇总 + 每条明细
|
|
|
|
|
|
|
+ Sheet 卡皇明细 :274584650 汇总 + 每条明细(2026/08/24 起同魔都:真实买家口径 + 进度里程碑 + 覆盖检测)
|
|
|
|
|
+ Sheet 用户排行榜(卡皇):274584650 买家榜(2026/08/24 新增)
|
|
|
|
|
+ Sheet 尼卡明细 :538252487 汇总 + 每条明细(2026/08/24 新增,同魔都扩展明细)
|
|
|
|
|
+ Sheet 用户排行榜(尼卡):538252487 买家榜(2026/08/24 新增)
|
|
|
Sheet 其他商家 :其余商家各一行汇总(中卡近似口径)
|
|
Sheet 其他商家 :其余商家各一行汇总(中卡近似口径)
|
|
|
注:原「魔都已售进度检测」独立 sheet 已于 2026/08/11 并入魔都明细(尾部到 25/50/75% 用时列)。
|
|
注:原「魔都已售进度检测」独立 sheet 已于 2026/08/11 并入魔都明细(尾部到 25/50/75% 用时列)。
|
|
|
|
|
+ 注:2026/08/24 起卡皇/尼卡也接入购买记录采集,明细升级为魔都同款扩展版并各带用户排行榜;
|
|
|
|
|
+ 三家统称 REAL_BUYER_MIDS(真实买家口径),其余商家仍走中卡近似。
|
|
|
|
|
|
|
|
口径说明:
|
|
口径说明:
|
|
|
- 销售额 = SUM(COALESCE(team_total_amount, sold_count * unit_price))
|
|
- 销售额 = SUM(COALESCE(team_total_amount, sold_count * unit_price))
|
|
|
随机团(选队随机/剩余随机)按 teams 逐队精算(team_total_amount,2026/08/11 起,
|
|
随机团(选队随机/剩余随机)按 teams 逐队精算(team_total_amount,2026/08/11 起,
|
|
|
见 docs/选队随机与剩余随机_总价口径与采集_20260811.md);固定价团回落原公式。
|
|
见 docs/选队随机与剩余随机_总价口径与采集_20260811.md);固定价团回落原公式。
|
|
|
- 成团数 = 该时段成交的拼团商品数
|
|
- 成团数 = 该时段成交的拼团商品数
|
|
|
- - 参与人数(魔都汇总 & 明细口径) = deca_buy_record 去重买家 user_id(真实参团人头;仅 881226408
|
|
|
|
|
- 采了购买记录)。魔都汇总「参与人数(真实买家)」= 跨其全部成交团去重(2026/08/14 起由
|
|
|
|
|
- 中卡近似切为真实买家);明细「参与人数(本团)」= 各团单独去重,故明细逐团相加(人次) ≥ 汇总。
|
|
|
|
|
|
|
+ - 参与人数(魔都/卡皇/尼卡 汇总 & 明细口径) = deca_buy_record 去重买家 user_id(真实参团人头;
|
|
|
|
|
+ REAL_BUYER_MIDS 三家采了购买记录,2026/08/24 起由仅魔都扩为三家)。各家汇总
|
|
|
|
|
+ 「参与人数(真实买家)」= 跨其全部成交团去重;明细「参与人数(本团)」= 各团单独去重,
|
|
|
|
|
+ 故明细逐团相加(人次) ≥ 汇总。
|
|
|
- 中卡用户数(近似)(平台大盘/其他商家口径) = 拆卡报告 hit_user_nickname 去重(仅覆盖 report_state=1
|
|
- 中卡用户数(近似)(平台大盘/其他商家口径) = 拆卡报告 hit_user_nickname 去重(仅覆盖 report_state=1
|
|
|
有报告的商品;这些商家未采购买记录,只能用中卡用户近似,非真实参团人头,偏低)
|
|
有报告的商品;这些商家未采购买记录,只能用中卡用户近似,非真实参团人头,偏低)
|
|
|
- 均拼单价 = 销售额 / 成团数
|
|
- 均拼单价 = 销售额 / 成团数
|
|
|
- - 人均消费 = 销售额 / 参与人数(魔都为真实买家;平台/其他商家为按中卡近似,偏高,仅供参考)
|
|
|
|
|
|
|
+ - 人均消费 = 销售额 / 参与人数(魔都/卡皇/尼卡为真实买家;平台/其他商家为按中卡近似,偏高,仅供参考)
|
|
|
- 卡密表 deca_kami_record 当前为空(FILL_KAMI 关),故无「球队」维度,明细按商品维度出。
|
|
- 卡密表 deca_kami_record 当前为空(FILL_KAMI 关),故无「球队」维度,明细按商品维度出。
|
|
|
|
|
|
|
|
从项目根目录运行:python stats/daily_report.py(cwd=根目录,读根目录 application.yml)
|
|
从项目根目录运行:python stats/daily_report.py(cwd=根目录,读根目录 application.yml)
|
|
|
"""
|
|
"""
|
|
|
import os
|
|
import os
|
|
|
|
|
+import re
|
|
|
import sys
|
|
import sys
|
|
|
import time
|
|
import time
|
|
|
# 把项目根目录加入 import 路径:企微发送模块 auto_send_wx_msg.py 只在根目录留一份(WEBHOOK_URL 单点维护)
|
|
# 把项目根目录加入 import 路径:企微发送模块 auto_send_wx_msg.py 只在根目录留一份(WEBHOOK_URL 单点维护)
|
|
@@ -57,11 +64,17 @@ logger.add("./logs/daily_report_{time:YYYYMMDD}.log", encoding="utf-8", rotation
|
|
|
# 企微发送:报告生成后把 Excel 发到企业微信群机器人(只发表格,不发图;群由 auto_send_wx_msg.WEBHOOK_URL 决定)
|
|
# 企微发送:报告生成后把 Excel 发到企业微信群机器人(只发表格,不发图;群由 auto_send_wx_msg.WEBHOOK_URL 决定)
|
|
|
SEND_WECHAT = True
|
|
SEND_WECHAT = True
|
|
|
|
|
|
|
|
-# 两个要出「汇总 + 明细」的重点商家;其余商家统一进「其他商家汇总」
|
|
|
|
|
-FOCUS_MERCHANTS = ["881226408", "274584650"]
|
|
|
|
|
-# 魔都兄弟球星卡:其每条明细走扩展版——多「参与人数」列(deca_buy_record 去重买家),
|
|
|
|
|
-# 尾部并入售卖进度里程碑(从 progress 表算到 25/50/75% 各用了多久;首张快照已越过阈值则留空)。仅本商家如此。
|
|
|
|
|
-MODDU_MID = "881226408"
|
|
|
|
|
|
|
+# 要出「汇总 + 明细」的重点商家;其余商家统一进「其他商家汇总」
|
|
|
|
|
+# (2026/08/24 新增尼卡拆卡 538252487;2026/08/25 新增文泰卡屋 591544726)
|
|
|
|
|
+FOCUS_MERCHANTS = ["881226408", "274584650", "538252487", "591544726"]
|
|
|
|
|
+# 已采真实购买记录(deca_buy_record)、可用「真实买家去重」口径的商家集合。
|
|
|
|
|
+# 2026/08/24:由单商家(仅魔都)扩为三家;2026/08/25:再加文泰——购买记录爬虫 buy_record_spider 现已并行采
|
|
|
|
|
+# 魔都/卡皇/尼卡/文泰,故这些家的:汇总参与人数(真实买家去重)、明细「参与人数(本团)」列、售卖进度里程碑
|
|
|
|
|
+# (到25/50/75%用时)、购买记录覆盖检测、用户排行榜,全部走真实买家口径(原仅魔都如此)。其余商家仍走中卡近似。
|
|
|
|
|
+REAL_BUYER_MIDS = {"881226408", "274584650", "538252487", "591544726"}
|
|
|
|
|
+# 商家 ID → 简称:用于「用户排行榜(简称)」的 sheet 名与标题、覆盖检测提示文案
|
|
|
|
|
+MERCHANT_SHORT_NAMES = {"881226408": "魔都", "274584650": "卡皇", "538252487": "尼卡", "591544726": "文泰"}
|
|
|
|
|
+MODDU_MID = "881226408" # 保留:魔都为首个接入真实购买记录的商家,扩展明细列结构/sheet 顺序以其为基准
|
|
|
OUT_PREFIX = "得卡已售每日报告" # 输出文件名前缀,实际文件名后缀加运行当天日期
|
|
OUT_PREFIX = "得卡已售每日报告" # 输出文件名前缀,实际文件名后缀加运行当天日期
|
|
|
|
|
|
|
|
# 时间窗过滤(p 别名):[昨天17:00, 今天06:00](2026/08/15 由 03:00 延到 06:00,凌晨仍在播)
|
|
# 时间窗过滤(p 别名):[昨天17:00, 今天06:00](2026/08/15 由 03:00 延到 06:00,凌晨仍在播)
|
|
@@ -77,9 +90,10 @@ DETAIL_COLS = [
|
|
|
("中卡人数", "中卡人数", False), # 该团拆卡报告 hit_user_nickname 去重(中卡近似),放开售时间前
|
|
("中卡人数", "中卡人数", False), # 该团拆卡报告 hit_user_nickname 去重(中卡近似),放开售时间前
|
|
|
("开售时间", "开售时间", False), ("成交时间", "成交时间", False), ("售卖时长", "售卖时长", False),
|
|
("开售时间", "开售时间", False), ("成交时间", "成交时间", False), ("售卖时长", "售卖时长", False),
|
|
|
]
|
|
]
|
|
|
-# 魔都(881226408)专属明细:在「中卡人数」前插「参与人数」(deca_buy_record 去重买家 user_id),
|
|
|
|
|
-# 并在尾部并入售卖进度里程碑(到 25/50/75% 用时,源 deca_onsale_product_progress_record)。
|
|
|
|
|
-# 原「魔都已售进度检测」独立 sheet 于 2026/08/11 并入本明细,不再单独出 sheet。
|
|
|
|
|
|
|
+# 已采购买记录商家(REAL_BUYER_MIDS:魔都/卡皇/尼卡)扩展明细:在「中卡人数」前插「参与人数」
|
|
|
|
|
+# (deca_buy_record 去重买家 user_id),并在尾部并入售卖进度里程碑(到 25/50/75% 用时,源
|
|
|
|
|
+# deca_onsale_product_progress_record)。原「魔都已售进度检测」独立 sheet 于 2026/08/11 并入本明细。
|
|
|
|
|
+# 常量名沿用 MODDU 前缀(历史沿革),2026/08/24 起卡皇/尼卡明细也复用此列规格。
|
|
|
MODDU_DETAIL_COLS = [
|
|
MODDU_DETAIL_COLS = [
|
|
|
("序号", "序号", False), ("团名(商品标题)", "团名", False),
|
|
("序号", "序号", False), ("团名(商品标题)", "团名", False),
|
|
|
("系列", "系列", False), ("类型", "类型", False), ("单价", "单价", True),
|
|
("系列", "系列", False), ("类型", "类型", False), ("单价", "单价", True),
|
|
@@ -95,16 +109,17 @@ SECTION_SPAN = len(MODDU_DETAIL_COLS)
|
|
|
# 汇总表指标键(商家/平台,dict 取值键,与显示标签解耦)
|
|
# 汇总表指标键(商家/平台,dict 取值键,与显示标签解耦)
|
|
|
SUMMARY_HEADERS = ["销售额", "成团数", "参与人数", "均拼单价", "人均消费"]
|
|
SUMMARY_HEADERS = ["销售额", "成团数", "参与人数", "均拼单价", "人均消费"]
|
|
|
|
|
|
|
|
-# 平台大盘竖排汇总行:(显示标签, dict取值键)。参与人数为「魔都真实买家 + 其他商家中卡去重」
|
|
|
|
|
-# 的混合口径(2026/08/14 起,见 fetch_platform_summary),故标签显式标注,避免误当纯真实人头。
|
|
|
|
|
|
|
+# 平台大盘竖排汇总行:(显示标签, dict取值键)。参与人数为「REAL_BUYER_MIDS(魔都/卡皇/尼卡/文泰…)真实买家
|
|
|
|
|
+# + 其他商家中卡去重」的混合口径(2026/08/14 起、真实买家逐步扩到多家,见 fetch_platform_summary),
|
|
|
|
|
+# 标签用「重点商家真实」不写死家数,新增商家无需再改此处,避免误当纯真实人头。
|
|
|
PLATFORM_ROWS = [
|
|
PLATFORM_ROWS = [
|
|
|
("商家数", "商家数"), ("销售额", "销售额"), ("成团数", "成团数"),
|
|
("商家数", "商家数"), ("销售额", "销售额"), ("成团数", "成团数"),
|
|
|
- ("参与人数(魔都真实+其他中卡)", "参与人数"), ("均拼单价", "均拼单价"),
|
|
|
|
|
|
|
+ ("参与人数(重点商家真实+其他中卡)", "参与人数"), ("均拼单价", "均拼单价"),
|
|
|
("人均消费", "人均消费"),
|
|
("人均消费", "人均消费"),
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
# 其他商家汇总表列:(显示表头, dict取值键, 是否金额格式)。这些商家未采购买记录,参与人数
|
|
# 其他商家汇总表列:(显示表头, dict取值键, 是否金额格式)。这些商家未采购买记录,参与人数
|
|
|
-# 只能用中卡用户近似,故表头标注「(近似)」,与魔都真实买家口径区分。
|
|
|
|
|
|
|
+# 只能用中卡用户近似,故表头标注「(近似)」,与魔都/卡皇/尼卡的真实买家口径区分。
|
|
|
OTHER_COLS = [
|
|
OTHER_COLS = [
|
|
|
("商家名", "商家名", False), ("销售额", "销售额", True), ("成团数", "成团数", False),
|
|
("商家名", "商家名", False), ("销售额", "销售额", True), ("成团数", "成团数", False),
|
|
|
("中卡用户数(近似)", "参与人数", False), ("均拼单价", "均拼单价", True),
|
|
("中卡用户数(近似)", "参与人数", False), ("均拼单价", "均拼单价", True),
|
|
@@ -117,7 +132,7 @@ WIN_P_YDAY = ("p.completed_at >= (CURDATE() - INTERVAL 2 DAY) + INTERVAL 17 HOUR
|
|
|
"AND p.completed_at <= (CURDATE() - INTERVAL 1 DAY) + INTERVAL 6 HOUR")
|
|
"AND p.completed_at <= (CURDATE() - INTERVAL 1 DAY) + INTERVAL 6 HOUR")
|
|
|
TOP_SERIES = 15 # 产品系列销售榜展示条数
|
|
TOP_SERIES = 15 # 产品系列销售榜展示条数
|
|
|
TOP_MERCHANT = 10 # 商家 GMV 榜展示条数(监测清单要「GMV前十商家」)
|
|
TOP_MERCHANT = 10 # 商家 GMV 榜展示条数(监测清单要「GMV前十商家」)
|
|
|
-TOP_USERS = None # 魔都用户排行榜展示条数(按参与金额倒序取前 N;仅魔都采了购买记录)
|
|
|
|
|
|
|
+TOP_USERS = None # 用户排行榜展示条数(按参与金额倒序取前 N;魔都/卡皇/尼卡各出一榜,None=全展示)
|
|
|
CONC_TOPS = (1, 3, 5, 10) # GMV 集中度统计的 TopN 档(Top1/3/5/10 占平台总 GMV)
|
|
CONC_TOPS = (1, 3, 5, 10) # GMV 集中度统计的 TopN 档(Top1/3/5/10 占平台总 GMV)
|
|
|
HOUR_DIST_DAYS = 7 # 组齐时段分布回看天数(反映平台 24h 组齐节奏)
|
|
HOUR_DIST_DAYS = 7 # 组齐时段分布回看天数(反映平台 24h 组齐节奏)
|
|
|
|
|
|
|
@@ -193,9 +208,10 @@ def get_window(pool) -> tuple[str, str]:
|
|
|
def fetch_platform_summary(pool) -> dict:
|
|
def fetch_platform_summary(pool) -> dict:
|
|
|
"""统计平台大盘汇总(时间窗内全部已售商品)。
|
|
"""统计平台大盘汇总(时间窗内全部已售商品)。
|
|
|
|
|
|
|
|
- 参与人数为混合口径(2026/08/14 起):魔都(881226408)采了真实购买记录,用 deca_buy_record
|
|
|
|
|
- 去重真实买家;其余商家未采购买记录,仍用拆卡报告 hit_user_nickname 去重的中卡用户近似。
|
|
|
|
|
- 两部分人群标识不同(魔都=user_id,其他=昵称)、无法跨口径去重,故直接相加,属近似上界。
|
|
|
|
|
|
|
+ 参与人数为混合口径(2026/08/14 起;2026/08/24 真实买家由仅魔都扩到魔都/卡皇/尼卡三家):
|
|
|
|
|
+ REAL_BUYER_MIDS 三家采了真实购买记录,用 deca_buy_record 去重真实买家;其余商家未采购买记录,
|
|
|
|
|
+ 仍用拆卡报告 hit_user_nickname 去重的中卡用户近似。两部分人群标识不同(真实买家=user_id,
|
|
|
|
|
+ 其他=昵称)、且三家真实买家之间也未跨商家去重(同一 user_id 跨家买会各记一次),故直接相加,属近似上界。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
@@ -203,25 +219,28 @@ def fetch_platform_summary(pool) -> dict:
|
|
|
Returns:
|
|
Returns:
|
|
|
dict: 含 销售额/商家数/成团数/参与人数/均拼单价/人均消费 六项。
|
|
dict: 含 销售额/商家数/成团数/参与人数/均拼单价/人均消费 六项。
|
|
|
"""
|
|
"""
|
|
|
|
|
+ real_mids = list(REAL_BUYER_MIDS)
|
|
|
|
|
+ ph = ",".join(["%s"] * len(real_mids)) # 中卡子查询要排除全部真实买家商家
|
|
|
sql = f"""
|
|
sql = f"""
|
|
|
SELECT
|
|
SELECT
|
|
|
ROUND(SUM(COALESCE(p.team_total_amount, p.sold_count * p.unit_price)), 2) AS amount,
|
|
ROUND(SUM(COALESCE(p.team_total_amount, p.sold_count * p.unit_price)), 2) AS amount,
|
|
|
COUNT(DISTINCT p.merchant_user_id) AS merchants,
|
|
COUNT(DISTINCT p.merchant_user_id) AS merchants,
|
|
|
COUNT(*) AS grp,
|
|
COUNT(*) AS grp,
|
|
|
- -- 其他商家(非魔都)中卡用户去重;魔都单独用真实买家,不计入此子查询
|
|
|
|
|
|
|
+ -- 非真实买家商家的中卡用户去重;魔都/卡皇/尼卡单独用真实买家,不计入此子查询
|
|
|
(SELECT COUNT(DISTINCT r.hit_user_nickname)
|
|
(SELECT COUNT(DISTINCT r.hit_user_nickname)
|
|
|
FROM deca_report_record r
|
|
FROM deca_report_record r
|
|
|
JOIN deca_product_record pp ON pp.product_code = r.product_code
|
|
JOIN deca_product_record pp ON pp.product_code = r.product_code
|
|
|
- WHERE pp.merchant_user_id <> %s
|
|
|
|
|
|
|
+ WHERE pp.merchant_user_id NOT IN ({ph})
|
|
|
AND pp.completed_at >= (CURDATE() - INTERVAL 1 DAY) + INTERVAL 17 HOUR
|
|
AND pp.completed_at >= (CURDATE() - INTERVAL 1 DAY) + INTERVAL 17 HOUR
|
|
|
AND pp.completed_at <= CURDATE() + INTERVAL 6 HOUR
|
|
AND pp.completed_at <= CURDATE() + INTERVAL 6 HOUR
|
|
|
AND r.hit_user_nickname IS NOT NULL AND r.hit_user_nickname <> '') AS others_people
|
|
AND r.hit_user_nickname IS NOT NULL AND r.hit_user_nickname <> '') AS others_people
|
|
|
FROM deca_product_record p
|
|
FROM deca_product_record p
|
|
|
WHERE {WIN_P} AND p.unit_price IS NOT NULL AND p.sold_count IS NOT NULL
|
|
WHERE {WIN_P} AND p.unit_price IS NOT NULL AND p.sold_count IS NOT NULL
|
|
|
"""
|
|
"""
|
|
|
- amount, merchants, groups, others_people = pool.select_all(sql, (MODDU_MID,))[0]
|
|
|
|
|
- # 平台参与人数 = 魔都真实买家(deca_buy_record 去重) + 其他商家中卡用户去重
|
|
|
|
|
- people = _fetch_real_buyers(pool, MODDU_MID) + (others_people or 0)
|
|
|
|
|
|
|
+ amount, merchants, groups, others_people = pool.select_all(sql, tuple(real_mids))[0]
|
|
|
|
|
+ # 平台参与人数 = 三家真实买家(deca_buy_record 各自去重后求和) + 其他商家中卡用户去重
|
|
|
|
|
+ real_people = sum(_fetch_real_buyers(pool, m) for m in real_mids)
|
|
|
|
|
+ people = real_people + (others_people or 0)
|
|
|
return _pack_summary(amount, groups, people, extra={"商家数": merchants})
|
|
return _pack_summary(amount, groups, people, extra={"商家数": merchants})
|
|
|
|
|
|
|
|
|
|
|
|
@@ -253,9 +272,9 @@ def fetch_merchant_summary(pool, mid: str) -> dict:
|
|
|
"""
|
|
"""
|
|
|
row = pool.select_all(sql, (mid, mid))
|
|
row = pool.select_all(sql, (mid, mid))
|
|
|
mname, amount, groups, people = row[0] if row else (None, None, 0, 0)
|
|
mname, amount, groups, people = row[0] if row else (None, None, 0, 0)
|
|
|
- # 魔都(881226408)采了真实购买记录:参与人数改用 deca_buy_record 去重真实买家,人均消费随之
|
|
|
|
|
- # 按真实人头计(覆盖上面 people 的中卡近似值);其余重点商家无购买记录,仍沿用中卡近似。
|
|
|
|
|
- if mid == MODDU_MID:
|
|
|
|
|
|
|
+ # 魔都/卡皇/尼卡(REAL_BUYER_MIDS)采了真实购买记录:参与人数改用 deca_buy_record 去重真实买家,
|
|
|
|
|
+ # 人均消费随之按真实人头计(覆盖上面 people 的中卡近似值);其余重点商家无购买记录,仍沿用中卡近似。
|
|
|
|
|
+ if mid in REAL_BUYER_MIDS:
|
|
|
people = _fetch_real_buyers(pool, mid)
|
|
people = _fetch_real_buyers(pool, mid)
|
|
|
d = _pack_summary(amount, groups, people)
|
|
d = _pack_summary(amount, groups, people)
|
|
|
d["商家名"] = mname or mid
|
|
d["商家名"] = mname or mid
|
|
@@ -371,7 +390,8 @@ def _pack_summary(amount, groups, people, extra: dict = None) -> dict:
|
|
|
def _fetch_real_buyers(pool, mid: str) -> int:
|
|
def _fetch_real_buyers(pool, mid: str) -> int:
|
|
|
"""查某商家时间窗内 deca_buy_record 去重真实买家数(跨其全部成交团)。
|
|
"""查某商家时间窗内 deca_buy_record 去重真实买家数(跨其全部成交团)。
|
|
|
|
|
|
|
|
- 仅魔都(881226408)采了真实购买记录,故只有它能用此口径;其余商家该表无数据、返回 0。
|
|
|
|
|
|
|
+ 魔都/卡皇/尼卡(REAL_BUYER_MIDS)采了真实购买记录,可用此口径(2026/08/24 由仅魔都扩为三家);
|
|
|
|
|
+ 其余商家该表无数据、返回 0。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
@@ -396,7 +416,7 @@ def _summary_rows(is_real: bool) -> list[tuple]:
|
|
|
"""按参与人数口径生成重点商家竖排汇总的(显示标签, 取值键)行规格。
|
|
"""按参与人数口径生成重点商家竖排汇总的(显示标签, 取值键)行规格。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
- is_real (bool): True=该商家参与人数为 deca_buy_record 真实买家(魔都),标签用
|
|
|
|
|
|
|
+ is_real (bool): True=该商家参与人数为 deca_buy_record 真实买家(魔都/卡皇/尼卡),标签用
|
|
|
「参与人数(真实买家)」;False=中卡用户近似,标签用「中卡用户数(近似)」,人均消费
|
|
「参与人数(真实买家)」;False=中卡用户近似,标签用「中卡用户数(近似)」,人均消费
|
|
|
标签相应标注「(按中卡近似)」。
|
|
标签相应标注「(按中卡近似)」。
|
|
|
|
|
|
|
@@ -410,7 +430,10 @@ def _summary_rows(is_real: bool) -> list[tuple]:
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_moddu_details(pool, mid: str) -> list[dict]:
|
|
def fetch_moddu_details(pool, mid: str) -> list[dict]:
|
|
|
- """取「魔都」商家(mid)时间窗内每个拼团(组队)的扩展明细,按总金额倒序。
|
|
|
|
|
|
|
+ """取某「已采真实购买记录」商家(mid)时间窗内每个拼团(组队)的扩展明细,按总金额倒序。
|
|
|
|
|
+
|
|
|
|
|
+ 2026/08/24:函数名沿用 moddu(魔都),但已泛化到 REAL_BUYER_MIDS 三家(魔都/卡皇/尼卡)——
|
|
|
|
|
+ SQL 全按 mid 参数查、对任意商家成立,故复用同一函数、按传入 mid 出各商家扩展明细。
|
|
|
|
|
|
|
|
在标准明细基础上多两类字段(原「魔都已售进度检测」独立 sheet 于 2026/08/11 并入此处):
|
|
在标准明细基础上多两类字段(原「魔都已售进度检测」独立 sheet 于 2026/08/11 并入此处):
|
|
|
- 参与人数:deca_buy_record 去重买家 user_id(真实参团人头;仅本商家采了购买记录)。
|
|
- 参与人数:deca_buy_record 去重买家 user_id(真实参团人头;仅本商家采了购买记录)。
|
|
@@ -478,7 +501,7 @@ def fetch_moddu_details(pool, mid: str) -> list[dict]:
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_moddu_user_ranking(pool, mid: str, top_n: int) -> tuple[list[dict], int]:
|
|
def fetch_moddu_user_ranking(pool, mid: str, top_n: int) -> tuple[list[dict], int]:
|
|
|
- """取「魔都」商家时间窗内的用户参与排行(按参与金额倒序,取前 top_n)。
|
|
|
|
|
|
|
+ """取某「已采真实购买记录」商家时间窗内的用户参与排行(按参与金额倒序,取前 top_n)。
|
|
|
|
|
|
|
|
「一个拼团商品 = 一辆车(组队)」,以 deca_buy_record 购买记录按买家 user_id 聚合:
|
|
「一个拼团商品 = 一辆车(组队)」,以 deca_buy_record 购买记录按买家 user_id 聚合:
|
|
|
- 参与车数 = COUNT(DISTINCT product_code),该买家窗口内参与的不同团数。
|
|
- 参与车数 = COUNT(DISTINCT product_code),该买家窗口内参与的不同团数。
|
|
@@ -486,7 +509,7 @@ def fetch_moddu_user_ranking(pool, mid: str, top_n: int) -> tuple[list[dict], in
|
|
|
「购买份数 × 团单价」估算;固定价团精确,随机团(选队随机/剩余随机)每队价不同,
|
|
「购买份数 × 团单价」估算;固定价团精确,随机团(选队随机/剩余随机)每队价不同,
|
|
|
此处按标称单价近似。同一买家在同一团的多条购买记录已由 SUM 累加。
|
|
此处按标称单价近似。同一买家在同一团的多条购买记录已由 SUM 累加。
|
|
|
- 车均消费 = 参与金额 ÷ 参与车数。
|
|
- 车均消费 = 参与金额 ÷ 参与车数。
|
|
|
- 仅魔都(881226408)采了购买记录,故只有它能出此榜。
|
|
|
|
|
|
|
+ 魔都/卡皇/尼卡(REAL_BUYER_MIDS)采了购买记录,均可出此榜(2026/08/24 由仅魔都扩为三家)。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
@@ -526,11 +549,12 @@ def fetch_moddu_user_ranking(pool, mid: str, top_n: int) -> tuple[list[dict], in
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_moddu_missing_teams(pool, mid: str) -> dict:
|
|
def fetch_moddu_missing_teams(pool, mid: str) -> dict:
|
|
|
- """对比「魔都」成交明细与购买记录覆盖,算出漏采购买记录的 T(团)。
|
|
|
|
|
|
|
+ """对比某「已采真实购买记录」商家成交明细与购买记录覆盖,算出漏采购买记录的 T(团)。
|
|
|
|
|
|
|
|
- 魔都明细每条 = 一个成交拼团商品(T),来自 deca_product_record;购买记录 deca_buy_record
|
|
|
|
|
- 是另路采集的。个别团在采到购买记录前就满仓成交下架,会「漏采」——本函数以时间窗内成交
|
|
|
|
|
- 团为基准,找出 deca_buy_record 里没有对应 product_code 的团,供魔都明细标注覆盖缺口。
|
|
|
|
|
|
|
+ 2026/08/24:函数名沿用 moddu,但已泛化到 REAL_BUYER_MIDS 三家(魔都/卡皇/尼卡),按 mid 参数查。
|
|
|
|
|
+ 明细每条 = 一个成交拼团商品(T),来自 deca_product_record;购买记录 deca_buy_record 是另路采集的。
|
|
|
|
|
+ 个别团在采到购买记录前就满仓成交下架,会「漏采」——本函数以时间窗内成交团为基准,找出
|
|
|
|
|
+ deca_buy_record 里没有对应 product_code 的团,供各商家明细标注覆盖缺口。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
@@ -778,28 +802,122 @@ def fetch_groupbuy_compare(pool) -> dict:
|
|
|
return {"today": _window_metrics(pool, WIN_P), "yday": _window_metrics(pool, WIN_P_YDAY)}
|
|
return {"today": _window_metrics(pool, WIN_P), "yday": _window_metrics(pool, WIN_P_YDAY)}
|
|
|
|
|
|
|
|
|
|
|
|
|
-def fetch_series_ranking(pool, top_n: int) -> tuple[list, float]:
|
|
|
|
|
- """取当日窗口内各产品系列的销售榜(按 GMV 倒序)及全窗口总 GMV(算占比用)。
|
|
|
|
|
|
|
+def classify_category(title: str, series: str = "") -> str:
|
|
|
|
|
+ """从标题(+系列名)判定得卡品类(平台无独立品类字段,只能按关键词判)。
|
|
|
|
|
+
|
|
|
|
|
+ 规则:优先中文运动词(篮球/NBA、足球/FIFA/世界杯、棒球/MLB、橄榄/NFL)——运动词多作标题前缀出现、判准率高;
|
|
|
|
|
+ 再判 TCG(宝可梦、海贼王、游戏王);都不中归「其他」。实测 2053 团仅 5 个落「其他」,且那 5 个标题本就写
|
|
|
|
|
+ 「其他运动」/为冷门 TCG(Weiss Schwarz),判类可靠。
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ title (str): 商品标题。
|
|
|
|
|
+ series (str, optional): 系列名(series_name),一并参与匹配。Defaults to ""。
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ str: 品类名(篮球/足球/棒球/橄榄球/宝可梦/海贼王/游戏王/其他)。
|
|
|
|
|
+ """
|
|
|
|
|
+ t = f"{title} {series}"
|
|
|
|
|
+ tl = t.lower()
|
|
|
|
|
+ if "篮球" in t or "nba" in tl:
|
|
|
|
|
+ return "篮球"
|
|
|
|
|
+ if "足球" in t or "fifa" in tl or "世界杯" in t or "英超" in t or "欧冠" in t:
|
|
|
|
|
+ return "足球"
|
|
|
|
|
+ if "棒球" in t or "mlb" in tl:
|
|
|
|
|
+ return "棒球"
|
|
|
|
|
+ if "橄榄" in t or "nfl" in tl:
|
|
|
|
|
+ return "橄榄球"
|
|
|
|
|
+ if (any(k in t for k in ["宝可梦", "寶可夢", "皮卡丘", "朋友派对", "乐园腾龙", "绿宝石",
|
|
|
|
|
+ "超级梦想", "超梦", "卡牌151", "黑白闪", "狂热", "朱紫"])
|
|
|
|
|
+ or re.search(r"\bsv\d", tl) or " ex " in f" {tl} "):
|
|
|
|
|
+ return "宝可梦"
|
|
|
|
|
+ if (any(k in t for k in ["海贼", "航海王", "路飞", "艾斯", "索隆", "娜美"])
|
|
|
|
|
+ or re.search(r"op-?\d", tl) or re.search(r"st-?\d", tl)):
|
|
|
|
|
+ return "海贼王"
|
|
|
|
|
+ if "游戏王" in t or "遊戲王" in t or "ygo" in tl:
|
|
|
|
|
+ return "游戏王"
|
|
|
|
|
+ return "其他"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def fetch_category_series(pool, top_n: int) -> tuple[list, float, list, float]:
|
|
|
|
|
+ """取当日窗口内「品类汇总」与「系列榜」(均按 GMV 倒序),供品类·系列榜 sheet。
|
|
|
|
|
+
|
|
|
|
|
+ 平台无品类字段,逐团用 classify_category(标题+系列) 判类后在 Python 聚合:
|
|
|
|
|
+ - 品类汇总:每品类的 成团数 / GMV(全部品类,倒序)。
|
|
|
|
|
+ - 系列榜:每系列的 成团数 / GMV / 所属品类(取该系列下出现团数最多的品类),按 GMV 取前 top_n。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
|
- top_n (int): 取前 N 个系列。
|
|
|
|
|
|
|
+ top_n (int): 系列榜取前 N。
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
- tuple[list, float]: (rows, total_gmv);rows 每项 (系列名, 成团数, GMV)。
|
|
|
|
|
|
|
+ tuple[list, float, list, float]: (cat_rows, cat_total, series_rows, series_total)。
|
|
|
|
|
+ cat_rows 每项 (品类, 成团数, GMV);series_rows 每项 (系列, 品类, 成团数, GMV);
|
|
|
|
|
+ 两个 total 为 GMV 合计(算占比分母,二者相等=全窗口 GMV)。
|
|
|
"""
|
|
"""
|
|
|
sql = f"""
|
|
sql = f"""
|
|
|
- SELECT COALESCE(NULLIF(p.series_name, ''), '(未标系列)') AS series,
|
|
|
|
|
- COUNT(*) AS grp,
|
|
|
|
|
- ROUND(SUM(COALESCE(p.team_total_amount, p.sold_count * p.unit_price)), 2) AS gmv
|
|
|
|
|
|
|
+ SELECT p.title, COALESCE(NULLIF(p.series_name, ''), '(未标系列)') AS series,
|
|
|
|
|
+ ROUND(COALESCE(p.team_total_amount, p.sold_count * p.unit_price), 2) AS gmv
|
|
|
FROM deca_product_record p
|
|
FROM deca_product_record p
|
|
|
WHERE {WIN_P} AND p.unit_price IS NOT NULL AND p.sold_count IS NOT NULL
|
|
WHERE {WIN_P} AND p.unit_price IS NOT NULL AND p.sold_count IS NOT NULL
|
|
|
- GROUP BY series
|
|
|
|
|
- ORDER BY gmv DESC
|
|
|
|
|
"""
|
|
"""
|
|
|
rows = pool.select_all(sql) or []
|
|
rows = pool.select_all(sql) or []
|
|
|
- total = sum(float(r[2]) for r in rows if r[2] is not None) # 全部系列合计(算占比分母)
|
|
|
|
|
- return rows[:top_n], total
|
|
|
|
|
|
|
+ cat_agg = {} # 品类 -> [成团数, GMV]
|
|
|
|
|
+ ser_agg = {} # 系列 -> {"grp": n, "gmv": x, "cat": {品类: 团数}}
|
|
|
|
|
+ for title, series, gmv in rows:
|
|
|
|
|
+ g = float(gmv) if gmv is not None else 0.0
|
|
|
|
|
+ cat = classify_category(title or "", series or "")
|
|
|
|
|
+ ca = cat_agg.setdefault(cat, [0, 0.0]); ca[0] += 1; ca[1] += g
|
|
|
|
|
+ sa = ser_agg.setdefault(series, {"grp": 0, "gmv": 0.0, "cat": {}})
|
|
|
|
|
+ sa["grp"] += 1; sa["gmv"] += g
|
|
|
|
|
+ sa["cat"][cat] = sa["cat"].get(cat, 0) + 1
|
|
|
|
|
+ total = round(sum(v[1] for v in cat_agg.values()), 2)
|
|
|
|
|
+ cat_rows = sorted([(k, v[0], round(v[1], 2)) for k, v in cat_agg.items()],
|
|
|
|
|
+ key=lambda x: x[2], reverse=True)
|
|
|
|
|
+ series_rows = sorted(
|
|
|
|
|
+ [(name, max(v["cat"], key=v["cat"].get), v["grp"], round(v["gmv"], 2))
|
|
|
|
|
+ for name, v in ser_agg.items()],
|
|
|
|
|
+ key=lambda x: x[3], reverse=True)[:top_n]
|
|
|
|
|
+ return cat_rows, total, series_rows, total
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# 需维护 category 品类字段的表:品类由 classify_category 从标题判定后落库,方便直接按品类查询/聚合(GROUP BY category)
|
|
|
|
|
+CATEGORY_TABLES = ("deca_product_record", "deca_onsale_product_record")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def backfill_category(pool, tables: tuple = CATEGORY_TABLES, only_null: bool = True) -> int:
|
|
|
|
|
+ """把品类判定结果(classify_category)落库到各表的 category 列,返回累计更新行数。
|
|
|
|
|
+
|
|
|
|
|
+ 平台无品类字段、品类靠标题判定;本函数将判定结果写入 category 列,供后续直接按品类查库(无需每次重算)。
|
|
|
|
|
+ 分类逻辑与报告共用 classify_category,口径一致。按品类分组、分块 UPDATE,减少 SQL 往返。
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ pool (MySQLConnectionPool): MySQL 连接池。
|
|
|
|
|
+ tables (tuple[str], optional): 要回填的表名。Defaults to CATEGORY_TABLES。
|
|
|
|
|
+ only_null (bool, optional): True 只补 category IS NULL 的行(日常报告前调用,仅补新增,成本低);
|
|
|
|
|
+ False 全量重算(分类规则调整后手动全刷一次)。Defaults to True。
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ int: 累计更新行数。
|
|
|
|
|
+ """
|
|
|
|
|
+ from collections import defaultdict
|
|
|
|
|
+ total = 0
|
|
|
|
|
+ for tbl in tables:
|
|
|
|
|
+ where = "WHERE category IS NULL" if only_null else ""
|
|
|
|
|
+ rows = pool.select_all(
|
|
|
|
|
+ f"SELECT product_code, title, COALESCE(series_name, '') FROM {tbl} {where}") or []
|
|
|
|
|
+ by_cat = defaultdict(list)
|
|
|
|
|
+ for code, title, series in rows:
|
|
|
|
|
+ if code is None:
|
|
|
|
|
+ continue
|
|
|
|
|
+ by_cat[classify_category(title or "", series or "")].append(code)
|
|
|
|
|
+ for cat, codes in by_cat.items():
|
|
|
|
|
+ for i in range(0, len(codes), 500): # 分块,避免超长 IN 列表
|
|
|
|
|
+ chunk = codes[i:i + 500]
|
|
|
|
|
+ ph = ",".join(["%s"] * len(chunk))
|
|
|
|
|
+ pool.update_one(
|
|
|
|
|
+ f"UPDATE {tbl} SET category=%s WHERE product_code IN ({ph})", (cat, *chunk))
|
|
|
|
|
+ total += sum(len(c) for c in by_cat.values())
|
|
|
|
|
+ return total
|
|
|
|
|
|
|
|
|
|
|
|
|
def fetch_merchant_gmv_ranking(pool) -> tuple[list, float, dict]:
|
|
def fetch_merchant_gmv_ranking(pool) -> tuple[list, float, dict]:
|
|
@@ -1052,22 +1170,33 @@ def _build_overview_sheet(ws, win: tuple, platform: dict, compare: dict,
|
|
|
r += 1
|
|
r += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _build_series_sheet(ws, series_rows: list, series_total: float):
|
|
|
|
|
- """构建「产品系列榜」sheet:当日各系列 GMV 榜 + 占比。
|
|
|
|
|
|
|
+def _build_category_series_sheet(ws, cat_rows: list, cat_total: float,
|
|
|
|
|
+ series_rows: list, series_total: float):
|
|
|
|
|
+ """构建「品类·系列榜」sheet:上段品类汇总(成团数/GMV/占比) + 下段系列榜(带品类列)。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
ws: openpyxl worksheet。
|
|
ws: openpyxl worksheet。
|
|
|
- series_rows (list): [(系列名, 成团数, GMV)]。
|
|
|
|
|
- series_total (float): 全窗口总 GMV(算占比分母)。
|
|
|
|
|
|
|
+ cat_rows (list): [(品类, 成团数, GMV)],按 GMV 倒序(全部品类)。
|
|
|
|
|
+ cat_total (float): 品类 GMV 合计(算占比分母)。
|
|
|
|
|
+ series_rows (list): [(系列, 品类, 成团数, GMV)],按 GMV 倒序、已截断 Top N。
|
|
|
|
|
+ series_total (float): 系列 GMV 合计(=全窗口 GMV,算占比分母)。
|
|
|
"""
|
|
"""
|
|
|
- _set_widths(ws, [36, 10, 16, 10])
|
|
|
|
|
- r = _write_section_title(ws, 1, f"产品系列销售榜(当日 Top{TOP_SERIES},按 GMV)", span=4)
|
|
|
|
|
- srows = [[name, int(g), float(gmv) if gmv is not None else 0,
|
|
|
|
|
- (float(gmv) / series_total if (series_total and gmv is not None) else None)]
|
|
|
|
|
- for name, g, gmv in series_rows]
|
|
|
|
|
- _write_hgrid(ws, r, ["系列", "成团数", "GMV", "占比"], srows,
|
|
|
|
|
- money_cols=(2,), pct_cols=(3,), start_col=1)
|
|
|
|
|
- ws.freeze_panes = "A3" # 冻结标题条 + 表头
|
|
|
|
|
|
|
+ _set_widths(ws, [36, 12, 16, 10, 10])
|
|
|
|
|
+ # 上段:品类汇总(成团数 + GMV + 占比)
|
|
|
|
|
+ r = _write_section_title(ws, 1, "品类汇总(当日,按 GMV 倒序;品类由标题判定)", span=5)
|
|
|
|
|
+ crows = [[c, int(g), float(gmv),
|
|
|
|
|
+ (float(gmv) / cat_total if cat_total else None)] for c, g, gmv in cat_rows]
|
|
|
|
|
+ r = _write_hgrid(ws, r, ["品类", "成团数", "GMV", "占比"], crows,
|
|
|
|
|
+ money_cols=(2,), pct_cols=(3,), start_col=1)
|
|
|
|
|
+ r += 1
|
|
|
|
|
+ # 下段:系列榜(比原版多「品类」列)
|
|
|
|
|
+ r = _write_section_title(ws, r, f"产品系列销售榜(当日 Top{TOP_SERIES},按 GMV)", span=5)
|
|
|
|
|
+ srows = [[name, cat, int(g), float(gmv),
|
|
|
|
|
+ (float(gmv) / series_total if series_total else None)]
|
|
|
|
|
+ for name, cat, g, gmv in series_rows]
|
|
|
|
|
+ _write_hgrid(ws, r, ["系列", "品类", "成团数", "GMV", "占比"], srows,
|
|
|
|
|
+ money_cols=(3,), pct_cols=(4,), start_col=1)
|
|
|
|
|
+ ws.freeze_panes = "A2" # 冻结顶部品类汇总标题
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_mrank_sheet(ws, mrank_rows: list, mrank_total: float):
|
|
def _build_mrank_sheet(ws, mrank_rows: list, mrank_total: float):
|
|
@@ -1109,7 +1238,8 @@ def _build_ops_sheet(ws, ops: list, hour_dist: list):
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_detail_sheet(ws, title: str, summ: dict, details: list, cols: list,
|
|
def _build_detail_sheet(ws, title: str, summ: dict, details: list, cols: list,
|
|
|
- is_real: bool, span: int, widths: list, miss_info: dict = None):
|
|
|
|
|
|
|
+ is_real: bool, span: int, widths: list, miss_info: dict = None,
|
|
|
|
|
+ short_name: str = None):
|
|
|
"""构建单个重点商家的明细 sheet:汇总(缩到 B/C 列) + 每条组队明细(从 A 列起)。
|
|
"""构建单个重点商家的明细 sheet:汇总(缩到 B/C 列) + 每条组队明细(从 A 列起)。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
@@ -1118,23 +1248,26 @@ def _build_detail_sheet(ws, title: str, summ: dict, details: list, cols: list,
|
|
|
summ (dict): 该商家汇总数据。
|
|
summ (dict): 该商家汇总数据。
|
|
|
details (list[dict]): 每条组队明细。
|
|
details (list[dict]): 每条组队明细。
|
|
|
cols (list[tuple]): 明细列规格(DETAIL_COLS / MODDU_DETAIL_COLS)。
|
|
cols (list[tuple]): 明细列规格(DETAIL_COLS / MODDU_DETAIL_COLS)。
|
|
|
- is_real (bool): 参与人数是否真实买家口径(魔都 True,其余 False)。
|
|
|
|
|
|
|
+ is_real (bool): 参与人数是否真实买家口径(魔都/卡皇/尼卡 True,其余 False)。
|
|
|
span (int): 标题条覆盖列数(= 明细列数)。
|
|
span (int): 标题条覆盖列数(= 明细列数)。
|
|
|
widths (list[float]): 各列宽度。
|
|
widths (list[float]): 各列宽度。
|
|
|
miss_info (dict, optional): 购买记录覆盖检测(fetch_moddu_missing_teams 返回);非 None
|
|
miss_info (dict, optional): 购买记录覆盖检测(fetch_moddu_missing_teams 返回);非 None
|
|
|
- 时在汇总块下方加「购买记录覆盖检测」小节,标注漏采多少 T 并列出漏团。仅魔都传入。
|
|
|
|
|
- Defaults to None。
|
|
|
|
|
|
|
+ 时在汇总块下方加「购买记录覆盖检测」小节,标注漏采多少 T 并列出漏团。仅已采购买记录的
|
|
|
|
|
+ 商家(魔都/卡皇/尼卡)传入。Defaults to None。
|
|
|
|
|
+ short_name (str, optional): 该商家简称(魔都/卡皇/尼卡),用于覆盖检测提示里指向对应
|
|
|
|
|
+ 「用户排行榜(简称)」sheet。Defaults to None。
|
|
|
"""
|
|
"""
|
|
|
_set_widths(ws, widths)
|
|
_set_widths(ws, widths)
|
|
|
r = _write_section_title(ws, 1, title, span=span)
|
|
r = _write_section_title(ws, 1, title, span=span)
|
|
|
# 汇总缩到 B/C 列:标签落宽的 B(团名列)、数值落 C,避开 A=序号 的窄列
|
|
# 汇总缩到 B/C 列:标签落宽的 B(团名列)、数值落 C,避开 A=序号 的窄列
|
|
|
r = _write_summary_block(ws, r, _summary_rows(is_real=is_real), summ, start_col=2)
|
|
r = _write_summary_block(ws, r, _summary_rows(is_real=is_real), summ, start_col=2)
|
|
|
r += 1
|
|
r += 1
|
|
|
- # 购买记录覆盖检测(仅魔都传入):成交团 vs 已采购买记录,漏采的 T 逐个列出(文本向右溢出显示)
|
|
|
|
|
|
|
+ # 购买记录覆盖检测(仅已采购买记录商家传入):成交团 vs 已采购买记录,漏采的 T 逐个列出(文本向右溢出显示)
|
|
|
if miss_info is not None:
|
|
if miss_info is not None:
|
|
|
r = _write_section_title(ws, r, "购买记录覆盖检测(成交团 vs 已采购买记录)", span=span)
|
|
r = _write_section_title(ws, r, "购买记录覆盖检测(成交团 vs 已采购买记录)", span=span)
|
|
|
|
|
+ rank_sheet = f"用户排行榜({short_name})" if short_name else "用户排行榜"
|
|
|
cov = (f"成交 {miss_info['成交团数']} 团 · 采到购买记录 {miss_info['有记录团数']} 团 · "
|
|
cov = (f"成交 {miss_info['成交团数']} 团 · 采到购买记录 {miss_info['有记录团数']} 团 · "
|
|
|
- f"漏采 {miss_info['漏采团数']} 团(用户排行见「用户排行榜(魔都)」sheet)")
|
|
|
|
|
|
|
+ f"漏采 {miss_info['漏采团数']} 团(用户排行见「{rank_sheet}」sheet)")
|
|
|
# 漏采 >0 时用深蓝加粗字提醒;0 时常规字
|
|
# 漏采 >0 时用深蓝加粗字提醒;0 时常规字
|
|
|
ws.cell(row=r, column=1, value=cov).font = FONT_HEADER if miss_info["漏采团数"] else FONT_CELL
|
|
ws.cell(row=r, column=1, value=cov).font = FONT_HEADER if miss_info["漏采团数"] else FONT_CELL
|
|
|
r += 1
|
|
r += 1
|
|
@@ -1152,19 +1285,22 @@ def _build_detail_sheet(ws, title: str, summ: dict, details: list, cols: list,
|
|
|
ws.freeze_panes = f"A{hdr_row + 1}" # 冻结到明细表头,滚动时表头常驻
|
|
ws.freeze_panes = f"A{hdr_row + 1}" # 冻结到明细表头,滚动时表头常驻
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _build_user_ranking_sheet(ws, rows: list, total_users: int, top_n: int):
|
|
|
|
|
- """构建「用户排行榜(魔都)」sheet:按参与金额倒序的买家榜(参与车数/参与金额/车均消费)。
|
|
|
|
|
|
|
+def _build_user_ranking_sheet(ws, rows: list, total_users: int, top_n: int, short_name: str = "魔都"):
|
|
|
|
|
+ """构建「用户排行榜(简称)」sheet:按参与金额倒序的买家榜(参与车数/参与金额/车均消费)。
|
|
|
|
|
+
|
|
|
|
|
+ 2026/08/24:由仅魔都泛化到魔都/卡皇/尼卡三家,靠 short_name 区分标题与所属商家。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
ws: openpyxl worksheet。
|
|
ws: openpyxl worksheet。
|
|
|
rows (list[dict]): 用户排行数据(fetch_moddu_user_ranking 返回,已倒序截断)。
|
|
rows (list[dict]): 用户排行数据(fetch_moddu_user_ranking 返回,已倒序截断)。
|
|
|
- total_users (int): 窗口内魔都全部参与买家数(用于标题展示)。
|
|
|
|
|
|
|
+ total_users (int): 窗口内该商家全部参与买家数(用于标题展示)。
|
|
|
top_n (int): 榜单展示上限(用于标题展示)。
|
|
top_n (int): 榜单展示上限(用于标题展示)。
|
|
|
|
|
+ short_name (str, optional): 商家简称(魔都/卡皇/尼卡),用于标题。Defaults to "魔都"。
|
|
|
"""
|
|
"""
|
|
|
_set_widths(ws, [8, 22, 16, 12, 16, 14])
|
|
_set_widths(ws, [8, 22, 16, 12, 16, 14])
|
|
|
cap = "全部展示" if top_n is None else f"取前 {min(len(rows), top_n)}"
|
|
cap = "全部展示" if top_n is None else f"取前 {min(len(rows), top_n)}"
|
|
|
r = _write_section_title(
|
|
r = _write_section_title(
|
|
|
- ws, 1, f"用户排行榜 · 魔都(共 {total_users} 人参与,{cap},按参与金额倒序)", span=6)
|
|
|
|
|
|
|
+ ws, 1, f"用户排行榜 · {short_name}(共 {total_users} 人参与,{cap},按参与金额倒序)", span=6)
|
|
|
grid = [[i + 1, d["用户昵称"], d["user_id"], d["参与车数"], d["参与金额"], d["车均消费"]]
|
|
grid = [[i + 1, d["用户昵称"], d["user_id"], d["参与车数"], d["参与金额"], d["车均消费"]]
|
|
|
for i, d in enumerate(rows)]
|
|
for i, d in enumerate(rows)]
|
|
|
r = _write_hgrid(ws, r, ["排名", "用户昵称", "user_id", "参与车数", "参与金额", "车均消费"],
|
|
r = _write_hgrid(ws, r, ["排名", "用户昵称", "user_id", "参与车数", "参与金额", "车均消费"],
|
|
@@ -1213,44 +1349,63 @@ def _build_others_sheet(ws, others: list):
|
|
|
DETAIL_WIDTHS_MODDU = [8, 48, 16, 11, 13, 9, 8, 14, 14, 19, 19, 12, 11, 11, 11] # 15 列(含里程碑)
|
|
DETAIL_WIDTHS_MODDU = [8, 48, 16, 11, 13, 9, 8, 14, 14, 19, 19, 12, 11, 11, 11] # 15 列(含里程碑)
|
|
|
DETAIL_WIDTHS_STD = [8, 48, 16, 11, 13, 9, 8, 14, 19, 19, 12] # 11 列(标准)
|
|
DETAIL_WIDTHS_STD = [8, 48, 16, 11, 13, 9, 8, 14, 19, 19, 12] # 11 列(标准)
|
|
|
# 重点商家 ID → 明细 sheet 名(其余走商家名兜底)
|
|
# 重点商家 ID → 明细 sheet 名(其余走商家名兜底)
|
|
|
-DETAIL_SHEET_NAMES = {"881226408": "魔都明细", "274584650": "卡皇明细"}
|
|
|
|
|
|
|
+DETAIL_SHEET_NAMES = {"881226408": "魔都明细", "274584650": "卡皇明细", "538252487": "尼卡明细", "591544726": "文泰明细"}
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_report(pool, out: str):
|
|
def build_report(pool, out: str):
|
|
|
"""汇总各段数据并生成多 Sheet Excel 报告(每一大项一个 sheet,各自独立列宽)。
|
|
"""汇总各段数据并生成多 Sheet Excel 报告(每一大项一个 sheet,各自独立列宽)。
|
|
|
|
|
|
|
|
- Sheet 顺序:平台总览 / 产品系列榜 / 商家GMV榜 / 运营节奏 / 魔都明细 / 用户排行榜(魔都) /
|
|
|
|
|
- 卡皇明细 / 其他商家。
|
|
|
|
|
|
|
+ Sheet 顺序(2026/08/24 起卡皇/尼卡、08/25 起文泰均走真实买家扩展明细 + 各带用户排行榜):
|
|
|
|
|
+ 平台总览 / 品类·系列榜 / 商家GMV榜 / 运营节奏 /
|
|
|
|
|
+ 魔都明细 / 用户排行榜(魔都) / 卡皇明细 / 用户排行榜(卡皇) / 尼卡明细 / 用户排行榜(尼卡) /
|
|
|
|
|
+ 文泰明细 / 用户排行榜(文泰) / 其他商家。
|
|
|
|
|
+ 规则:FOCUS_MERCHANTS 逐个出明细 sheet;属 REAL_BUYER_MIDS 的商家紧跟其「用户排行榜(简称)」。
|
|
|
|
|
|
|
|
Args:
|
|
Args:
|
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
pool (MySQLConnectionPool): MySQL 连接池。
|
|
|
out (str): 导出的 xlsx 路径。
|
|
out (str): 导出的 xlsx 路径。
|
|
|
"""
|
|
"""
|
|
|
|
|
+ # 报告前顺带把新成交团/在售品的品类落库到 category 列(方便按品类查库);只补 NULL 行、成本低
|
|
|
|
|
+ try:
|
|
|
|
|
+ n_cat = backfill_category(pool, only_null=True)
|
|
|
|
|
+ if n_cat:
|
|
|
|
|
+ logger.info(f"品类字段回填 {n_cat} 行(category IS NULL)")
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ logger.warning(f"品类字段回填跳过: {e}") # 回填失败不阻塞报告产出
|
|
|
win = get_window(pool)
|
|
win = get_window(pool)
|
|
|
platform = fetch_platform_summary(pool)
|
|
platform = fetch_platform_summary(pool)
|
|
|
compare = fetch_groupbuy_compare(pool) # 当日 vs 昨日组齐环比
|
|
compare = fetch_groupbuy_compare(pool) # 当日 vs 昨日组齐环比
|
|
|
- series_rows, series_total = fetch_series_ranking(pool, TOP_SERIES) # 产品系列销售榜
|
|
|
|
|
|
|
+ cat_rows, cat_total, series_rows, series_total = fetch_category_series(pool, TOP_SERIES) # 品类汇总 + 系列榜
|
|
|
mrank_rows, mrank_total, mrank_conc = fetch_merchant_gmv_ranking(pool) # 商家 GMV 榜 + 集中度
|
|
mrank_rows, mrank_total, mrank_conc = fetch_merchant_gmv_ranking(pool) # 商家 GMV 榜 + 集中度
|
|
|
ops = [fetch_focus_ops_snapshot(pool, mid) for mid in FOCUS_MERCHANTS] # 重点商家运营快照
|
|
ops = [fetch_focus_ops_snapshot(pool, mid) for mid in FOCUS_MERCHANTS] # 重点商家运营快照
|
|
|
hour_dist = fetch_completion_hour_dist(pool, HOUR_DIST_DAYS) # 组齐时段 24h 分布
|
|
hour_dist = fetch_completion_hour_dist(pool, HOUR_DIST_DAYS) # 组齐时段 24h 分布
|
|
|
- # 魔都(MODDU_MID)明细走扩展版(带参与人数 + 进度里程碑),其余重点商家走标准明细
|
|
|
|
|
|
|
+ # 已采购买记录的商家(REAL_BUYER_MIDS=魔都/卡皇/尼卡)明细走扩展版(带参与人数 + 进度里程碑),
|
|
|
|
|
+ # 其余重点商家走标准明细(中卡近似)
|
|
|
focus = []
|
|
focus = []
|
|
|
for mid in FOCUS_MERCHANTS:
|
|
for mid in FOCUS_MERCHANTS:
|
|
|
summ = fetch_merchant_summary(pool, mid)
|
|
summ = fetch_merchant_summary(pool, mid)
|
|
|
- if mid == MODDU_MID:
|
|
|
|
|
|
|
+ if mid in REAL_BUYER_MIDS:
|
|
|
focus.append((mid, summ, fetch_moddu_details(pool, mid), MODDU_DETAIL_COLS))
|
|
focus.append((mid, summ, fetch_moddu_details(pool, mid), MODDU_DETAIL_COLS))
|
|
|
else:
|
|
else:
|
|
|
focus.append((mid, summ, fetch_merchant_details(pool, mid), DETAIL_COLS))
|
|
focus.append((mid, summ, fetch_merchant_details(pool, mid), DETAIL_COLS))
|
|
|
others = fetch_other_merchants(pool, FOCUS_MERCHANTS)
|
|
others = fetch_other_merchants(pool, FOCUS_MERCHANTS)
|
|
|
- # 魔都用户排行(仅魔都采了购买记录) + 购买记录覆盖检测(成交团 vs 已采购买记录,看漏几个 T)
|
|
|
|
|
- moddu_user_rank, moddu_user_total = fetch_moddu_user_ranking(pool, MODDU_MID, TOP_USERS)
|
|
|
|
|
- moddu_miss = fetch_moddu_missing_teams(pool, MODDU_MID)
|
|
|
|
|
|
|
+ # 各已采购买记录商家各自算:用户排行(按参与金额倒序) + 购买记录覆盖检测(成交团 vs 已采购买记录,看漏几个 T)
|
|
|
|
|
+ rank_by_mid = {} # {mid: (rows, total_users)}
|
|
|
|
|
+ miss_by_mid = {} # {mid: miss_info}
|
|
|
|
|
+ for mid in FOCUS_MERCHANTS:
|
|
|
|
|
+ if mid in REAL_BUYER_MIDS:
|
|
|
|
|
+ rank_by_mid[mid] = fetch_moddu_user_ranking(pool, mid, TOP_USERS)
|
|
|
|
|
+ miss_by_mid[mid] = fetch_moddu_missing_teams(pool, mid)
|
|
|
|
|
|
|
|
- # 口径脚注(放平台总览底部;解释两种「参与人数」口径的差别)
|
|
|
|
|
|
|
+ # 口径脚注(放平台总览底部;解释两种「参与人数」口径的差别)。
|
|
|
|
|
+ # 真实买家口径商家名按 FOCUS 顺序取 REAL_BUYER_MIDS 的简称动态拼接,新增商家自动纳入文案、无需再改此处。
|
|
|
|
|
+ real_names = "/".join(MERCHANT_SHORT_NAMES.get(m, m) for m in FOCUS_MERCHANTS if m in REAL_BUYER_MIDS)
|
|
|
notes = [
|
|
notes = [
|
|
|
- "注:① 魔都兄弟球星卡「参与人数(真实买家)」= deca_buy_record 真实购买记录去重买家(跨其全部成交团);"
|
|
|
|
|
- "各商家明细「参与人数(本团)」为各团单独去重买家,故明细逐团相加(人次) ≥ 汇总(跨团去重人头)。",
|
|
|
|
|
- " ② 平台大盘参与人数 = 魔都真实买家 + 其他商家中卡去重(两口径人群标识不同、无法跨口径去重,直接相加,属近似上界)。",
|
|
|
|
|
|
|
+ f"注:① {real_names} 各家「参与人数(真实买家)」= deca_buy_record 真实购买记录去重买家(跨其全部成交团);"
|
|
|
|
|
+ "各商家明细「参与人数(本团)」为各团单独去重买家,故明细逐团相加(人次) ≥ 汇总(跨团去重人头)。"
|
|
|
|
|
+ "(新接入商家历史团购买记录可能为 0,随后续采集逐日补齐。)",
|
|
|
|
|
+ f" ② 平台大盘参与人数 = {real_names} 真实买家 + 其他商家中卡去重(两口径人群标识不同、且各真实买家商家间"
|
|
|
|
|
+ "也未跨商家去重,直接相加,属近似上界)。",
|
|
|
" ③ 其他商家未采购买记录,「中卡用户数(近似)」= 拆卡报告 hit_user_nickname 去重(仅报告命中/中卡用户,"
|
|
" ③ 其他商家未采购买记录,「中卡用户数(近似)」= 拆卡报告 hit_user_nickname 去重(仅报告命中/中卡用户,"
|
|
|
"非真实参团人头,偏低),其「人均消费(按中卡近似)」据此计算、偏高,仅供参考。",
|
|
"非真实参团人头,偏低),其「人均消费(按中卡近似)」据此计算、偏高,仅供参考。",
|
|
|
]
|
|
]
|
|
@@ -1259,23 +1414,27 @@ def build_report(pool, out: str):
|
|
|
ws = wb.active
|
|
ws = wb.active
|
|
|
ws.title = "平台总览"
|
|
ws.title = "平台总览"
|
|
|
_build_overview_sheet(ws, win, platform, compare, mrank_conc, notes)
|
|
_build_overview_sheet(ws, win, platform, compare, mrank_conc, notes)
|
|
|
- _build_series_sheet(wb.create_sheet("产品系列榜"), series_rows, series_total)
|
|
|
|
|
|
|
+ # 品类·系列榜(Excel sheet 名禁用「/」,故用中点「·」)
|
|
|
|
|
+ _build_category_series_sheet(wb.create_sheet("品类·系列榜"), cat_rows, cat_total, series_rows, series_total)
|
|
|
_build_mrank_sheet(wb.create_sheet("商家GMV榜"), mrank_rows, mrank_total)
|
|
_build_mrank_sheet(wb.create_sheet("商家GMV榜"), mrank_rows, mrank_total)
|
|
|
_build_ops_sheet(wb.create_sheet("运营节奏"), ops, hour_dist)
|
|
_build_ops_sheet(wb.create_sheet("运营节奏"), ops, hour_dist)
|
|
|
# 每个重点商家单独一个明细 sheet(各自独立列宽,互不迁就)
|
|
# 每个重点商家单独一个明细 sheet(各自独立列宽,互不迁就)
|
|
|
for (mid, summ, details, cols) in focus:
|
|
for (mid, summ, details, cols) in focus:
|
|
|
sheet_name = DETAIL_SHEET_NAMES.get(mid, f"{summ['商家名'][:8]}明细")
|
|
sheet_name = DETAIL_SHEET_NAMES.get(mid, f"{summ['商家名'][:8]}明细")
|
|
|
ws_d = wb.create_sheet(sheet_name)
|
|
ws_d = wb.create_sheet(sheet_name)
|
|
|
- is_real = (mid == MODDU_MID)
|
|
|
|
|
|
|
+ is_real = (mid in REAL_BUYER_MIDS)
|
|
|
widths = DETAIL_WIDTHS_MODDU if is_real else DETAIL_WIDTHS_STD
|
|
widths = DETAIL_WIDTHS_MODDU if is_real else DETAIL_WIDTHS_STD
|
|
|
- # 魔都明细尾部附「购买记录覆盖检测」(漏采团数);其余商家无购买记录、不检测
|
|
|
|
|
- miss = moddu_miss if mid == MODDU_MID else None
|
|
|
|
|
|
|
+ short = MERCHANT_SHORT_NAMES.get(mid)
|
|
|
|
|
+ # 已采购买记录商家的明细尾部附「购买记录覆盖检测」(漏采团数);其余商家无购买记录、不检测
|
|
|
|
|
+ miss = miss_by_mid.get(mid)
|
|
|
_build_detail_sheet(ws_d, f"{summ['商家名']} · 汇总(成交时间窗 {win[0]} ~ {win[1]})",
|
|
_build_detail_sheet(ws_d, f"{summ['商家名']} · 汇总(成交时间窗 {win[0]} ~ {win[1]})",
|
|
|
- summ, details, cols, is_real, len(cols), widths, miss_info=miss)
|
|
|
|
|
- # 魔都明细后紧跟「用户排行榜(魔都)」sheet,让魔都相关表相邻
|
|
|
|
|
- if mid == MODDU_MID:
|
|
|
|
|
- _build_user_ranking_sheet(wb.create_sheet("用户排行榜(魔都)"),
|
|
|
|
|
- moddu_user_rank, moddu_user_total, TOP_USERS)
|
|
|
|
|
|
|
+ summ, details, cols, is_real, len(cols), widths,
|
|
|
|
|
+ miss_info=miss, short_name=short)
|
|
|
|
|
+ # 已采购买记录商家的明细后紧跟其「用户排行榜(简称)」sheet,让同商家相关表相邻
|
|
|
|
|
+ if mid in REAL_BUYER_MIDS:
|
|
|
|
|
+ rank_rows, rank_total = rank_by_mid[mid]
|
|
|
|
|
+ _build_user_ranking_sheet(wb.create_sheet(f"用户排行榜({short})"),
|
|
|
|
|
+ rank_rows, rank_total, TOP_USERS, short_name=short)
|
|
|
_build_others_sheet(wb.create_sheet("其他商家"), others)
|
|
_build_others_sheet(wb.create_sheet("其他商家"), others)
|
|
|
|
|
|
|
|
wb.save(out)
|
|
wb.save(out)
|