feat(analysis): 网页汇总表自定义列
- analyze 支持 custom_columns:白名单表达式树由服务端 Decimal 求值, 自定义列进入汇总、分组行与明细(缺失保持不可用,不转零) - 新增「选择网页自定义列」弹窗:从定义库(含共享)多选最多 10 列, 应用后进入主分析表格与热力图;公式工作簿可一键导出网页已选列 - 请求去重纳入自定义列;Playwright 覆盖选择后显示与数值断言
This commit is contained in:
+44
-10
@@ -167,9 +167,9 @@ def metrics(agg, formula, products=None):
|
||||
}
|
||||
|
||||
|
||||
def comparison(current, baseline, comparable):
|
||||
def comparison(current, baseline, comparable, keys=None):
|
||||
result = {}
|
||||
for key in METRICS:
|
||||
for key in keys or METRICS:
|
||||
c, b = current.get(key), baseline.get(key)
|
||||
delta = c - b if comparable and c is not None and b is not None else None
|
||||
change = Decimal(delta) / abs(Decimal(b)) if delta is not None and b != 0 else None
|
||||
@@ -177,8 +177,31 @@ def comparison(current, baseline, comparable):
|
||||
return result
|
||||
|
||||
|
||||
def custom_columns(ds, body):
|
||||
from .formula_columns import ensure_available_metrics, validate_column
|
||||
|
||||
columns = body.get("custom_columns", [])
|
||||
if columns is None:
|
||||
return []
|
||||
if not isinstance(columns, list) or len(columns) > 10:
|
||||
raise ValueError("自定义列最多 10 个")
|
||||
result = [validate_column(column) for column in columns]
|
||||
for column in result:
|
||||
ensure_available_metrics(ds, column)
|
||||
return result
|
||||
|
||||
|
||||
def custom_values(values, columns):
|
||||
from .formula_columns import evaluate
|
||||
|
||||
return {f"custom_{index}": evaluate(column["expression"], values) for index, column in enumerate(columns)}
|
||||
|
||||
|
||||
def analyze(ds, member, body, preferences):
|
||||
ctx = context(ds, body)
|
||||
columns = custom_columns(ds, body)
|
||||
ctx = {**context(ds, body), "custom_columns": columns}
|
||||
custom_keys = [f"custom_{index}" for index in range(len(columns))]
|
||||
value_keys = [*METRICS, *custom_keys]
|
||||
qs = scoped_facts(ds, member, ctx["filters"])
|
||||
formula = ds.mapping["roi_formula"]
|
||||
products = ds.mapping.get("self_operated_products", [])
|
||||
@@ -190,8 +213,14 @@ def analyze(ds, member, body, preferences):
|
||||
)
|
||||
comparable = not ctx["missing"]
|
||||
if set(ctx["current"]) - set(ds.periods):
|
||||
total_c = {**total_c, **{key: None for key in METRICS}}
|
||||
summary = comparison(total_c, total_b, comparable)
|
||||
total_c = {**total_c, **{key: None for key in value_keys}}
|
||||
current_missing = bool(set(ctx["current"]) - set(ds.periods))
|
||||
total_c = {
|
||||
**total_c,
|
||||
**({key: None for key in custom_keys} if current_missing else custom_values(total_c, columns)),
|
||||
}
|
||||
total_b = {**total_b, **custom_values(total_b, columns)}
|
||||
summary = comparison(total_c, total_b, comparable, value_keys)
|
||||
group = "dimensions__" + ctx["group"]
|
||||
buckets = {}
|
||||
for side in ("current", "baseline"):
|
||||
@@ -205,7 +234,7 @@ def analyze(ds, member, body, preferences):
|
||||
k: Decimal(0)
|
||||
if k in ("revenue", "cost", "gross", "customers", "self_revenue") and total.get(k) is not None
|
||||
else None
|
||||
for k in METRICS
|
||||
for k in value_keys
|
||||
},
|
||||
}
|
||||
|
||||
@@ -217,8 +246,10 @@ def analyze(ds, member, body, preferences):
|
||||
for label, sides in buckets.items():
|
||||
c, b = sides.get("current", empty_side(total_c)), sides.get("baseline", empty_side(total_b))
|
||||
if set(ctx["current"]) - set(ds.periods):
|
||||
c = {**c, **{key: None for key in METRICS}}
|
||||
values = comparison(c, b, comparable)
|
||||
c = {**c, **{key: None for key in value_keys}}
|
||||
c = {**c, **({key: None for key in custom_keys} if current_missing else custom_values(c, columns))}
|
||||
b = {**b, **custom_values(b, columns)}
|
||||
values = comparison(c, b, comparable, value_keys)
|
||||
v = values[metric]
|
||||
change, delta = v["change"], v["delta"]
|
||||
impact = (
|
||||
@@ -249,7 +280,10 @@ def analyze(ds, member, body, preferences):
|
||||
)
|
||||
rows.sort(key=lambda r: (not r["anomaly"], -r["impact"], r["label"]))
|
||||
observed = {
|
||||
r["period"]: metrics(r, formula, products)
|
||||
r["period"]: {
|
||||
**(values := metrics(r, formula, products)),
|
||||
**custom_values(values, columns),
|
||||
}
|
||||
for r in qs.values("period").annotate(**aggregations(products))
|
||||
}
|
||||
trend = []
|
||||
@@ -257,7 +291,7 @@ def analyze(ds, member, body, preferences):
|
||||
period = month_text(index)
|
||||
values = observed.get(period)
|
||||
if values is None:
|
||||
values = empty_side(total_c) if period in ds.periods else {key: None for key in METRICS}
|
||||
values = empty_side(total_c) if period in ds.periods else {key: None for key in value_keys}
|
||||
trend.append({"period": period, **values})
|
||||
return {
|
||||
"dataset_id": str(ds.pk),
|
||||
|
||||
Reference in New Issue
Block a user