feat(analysis): 网页汇总表自定义列
Verify / verify (push) Failing after 1s
AI Review / review (pull_request) Successful in 14s
Verify / verify (pull_request) Failing after 0s

- analyze 支持 custom_columns:白名单表达式树由服务端 Decimal 求值,
  自定义列进入汇总、分组行与明细(缺失保持不可用,不转零)
- 新增「选择网页自定义列」弹窗:从定义库(含共享)多选最多 10 列,
  应用后进入主分析表格与热力图;公式工作簿可一键导出网页已选列
- 请求去重纳入自定义列;Playwright 覆盖选择后显示与数值断言
This commit is contained in:
Jayway Liu
2026-10-11 09:45:22 +08:00
parent 509b406254
commit adce23bbd9
7 changed files with 216 additions and 40 deletions
+44 -10
View File
@@ -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),