开发机迁移
This commit is contained in:
@@ -26,7 +26,7 @@ SCHEMA_ETL=meta
|
||||
# API 配置(上游 SaaS API)
|
||||
# ------------------------------------------------------------------------------
|
||||
API_BASE=https://pc.ficoo.vip/apiprod/admin/v1/
|
||||
API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnQtdHlwZSI6IjQiLCJ1c2VyLXR5cGUiOiIxIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiMTIiLCJyb2xlLWlkIjoiMTIiLCJ0ZW5hbnQtaWQiOiIyNzkwNjgzMTYwNzA5OTU3Iiwibmlja25hbWUiOiLnp5_miLfnrqHnkIblkZjvvJrmganmgakxIiwic2l0ZS1pZCI6IjAiLCJtb2JpbGUiOiIxMzgxMDUwMjMwNCIsInNpZCI6IjI5NTA0ODk2NTgzOTU4NDUiLCJzdGFmZi1pZCI6IjMwMDk5MTg2OTE1NTkwNDUiLCJvcmctaWQiOiIwIiwicm9sZS10eXBlIjoiMyIsInJlZnJlc2hUb2tlbiI6IkZJUUxIWWJLSFl5QktJQlVuLzZuQVdINitqOEpLbGNHN1NScDFFaFNuMEE9IiwicmVmcmVzaEV4cGlyeVRpbWUiOiIyMDI2LzQvNyDkuIrljYg0OjU2OjA4IiwibmVlZENoZWNrVG9rZW4iOiJmYWxzZSIsImV4cCI6MTc3NTUwODk2OCwiaXNzIjoidGVzdCIsImF1ZCI6IlVzZXIifQ.fc92FpZrLBkV9tD9sNPCvMvgNePh5Y7T6g5FLx8N16A
|
||||
API_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnQtdHlwZSI6IjQiLCJ1c2VyLXR5cGUiOiIxIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiMTIiLCJyb2xlLWlkIjoiMTIiLCJ0ZW5hbnQtaWQiOiIyNzkwNjgzMTYwNzA5OTU3Iiwibmlja25hbWUiOiLnp5_miLfnrqHnkIblkZjvvJrmganmgakxIiwic2l0ZS1pZCI6IjAiLCJtb2JpbGUiOiIxMzgxMDUwMjMwNCIsInNpZCI6IjI5NTA0ODk2NTgzOTU4NDUiLCJzdGFmZi1pZCI6IjMwMDk5MTg2OTE1NTkwNDUiLCJvcmctaWQiOiIwIiwicm9sZS10eXBlIjoiMyIsInJlZnJlc2hUb2tlbiI6Ik9OUTkreFhSWjFPVFhzQWhieTJVa3RyVXR6UzdldVE5Q1VVQ3QzQ1ArMlE9IiwicmVmcmVzaEV4cGlyeVRpbWUiOiIyMDI2LzQvMTQg5LiL5Y2INTo0MToyNSIsIm5lZWRDaGVja1Rva2VuIjoiZmFsc2UiLCJleHAiOjE3NzYxNTk2ODUsImlzcyI6InRlc3QiLCJhdWQiOiJVc2VyIn0.KyULo2a6dirmiAkka5Ocu_ieoZY5VsVWVqMj5smwvmE
|
||||
API_TIMEOUT=20
|
||||
API_PAGE_SIZE=200
|
||||
API_RETRY_MAX=3
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库连接管理器(限制最大连接超时时间)。"""
|
||||
|
||||
import time
|
||||
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
|
||||
# 连接重试参数:应对 PostgreSQL 瞬时不可用
|
||||
_CONNECT_MAX_RETRIES = 3
|
||||
_CONNECT_RETRY_DELAY = 1.0
|
||||
|
||||
|
||||
class DatabaseConnection:
|
||||
"""封装 psycopg2 连接,支持会话参数和超时保护。"""
|
||||
@@ -24,11 +30,25 @@ class DatabaseConnection:
|
||||
# assumptions: libpq 默认使用系统 locale 的 client_encoding,Windows 中文系统为 GBK/CP936
|
||||
# 边界: 显式指定 client_encoding=utf8 确保连接层始终使用 UTF-8,与数据库 server_encoding 一致
|
||||
# 验证: web-admin 手动触发 ETL 全量 flow,不再出现 0xd6 解码错误
|
||||
conn = psycopg2.connect(
|
||||
self._dsn,
|
||||
connect_timeout=timeout_val,
|
||||
options="-c client_encoding=utf8",
|
||||
)
|
||||
last_exc = None
|
||||
for attempt in range(_CONNECT_MAX_RETRIES):
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
self._dsn,
|
||||
connect_timeout=timeout_val,
|
||||
options="-c client_encoding=utf8",
|
||||
keepalives=1,
|
||||
keepalives_idle=60,
|
||||
keepalives_interval=10,
|
||||
keepalives_count=3,
|
||||
)
|
||||
break
|
||||
except psycopg2.OperationalError as e:
|
||||
last_exc = e
|
||||
if attempt < _CONNECT_MAX_RETRIES - 1:
|
||||
time.sleep(_CONNECT_RETRY_DELAY * (attempt + 1))
|
||||
else:
|
||||
raise last_exc
|
||||
conn.autocommit = False
|
||||
|
||||
# 会话参数(时区、语句超时等)
|
||||
|
||||
@@ -30,6 +30,16 @@ class DatabaseOperations:
|
||||
@property
|
||||
def _connect_timeout(self):
|
||||
return self._connection._connect_timeout
|
||||
|
||||
def ensure_open(self) -> bool:
|
||||
"""透传 DatabaseConnection.ensure_open(),重连后同步 self.conn 引用。"""
|
||||
result = self._connection.ensure_open()
|
||||
self.conn = self._connection.conn
|
||||
return result
|
||||
|
||||
def rollback(self):
|
||||
"""透传 DatabaseConnection.rollback()。"""
|
||||
self._connection.rollback()
|
||||
|
||||
def batch_execute(self, sql: str, rows: list, page_size: int = 1000):
|
||||
"""批量执行SQL"""
|
||||
|
||||
@@ -224,6 +224,9 @@ class FlowRunner:
|
||||
summary_text = flow_logger.end(status="成功")
|
||||
self.logger.info("\n%s", summary_text)
|
||||
|
||||
# CHANGE 2026-04-07 | Fix-12:ETL 完成后通知后端触发任务编排
|
||||
self._notify_backend_etl_completed(run_label)
|
||||
|
||||
return {
|
||||
"status": "SUCCESS",
|
||||
"flow": run_label,
|
||||
@@ -243,6 +246,33 @@ class FlowRunner:
|
||||
self.logger.error("\n%s", summary_text)
|
||||
raise
|
||||
|
||||
def _notify_backend_etl_completed(self, pipeline: str) -> None:
|
||||
"""ETL 完成后通知后端触发任务编排(recall_detector → task_generator)。
|
||||
|
||||
CHANGE 2026-04-07 | Fix-12:失败不阻断主流程,仅记录警告。
|
||||
"""
|
||||
import os
|
||||
backend_url = os.getenv("BACKEND_API_URL", "http://127.0.0.1:8000")
|
||||
internal_token = os.getenv("INTERNAL_API_TOKEN", "")
|
||||
if not internal_token:
|
||||
self.logger.warning("ETL 完成回调跳过:INTERNAL_API_TOKEN 未配置")
|
||||
return
|
||||
|
||||
try:
|
||||
import httpx
|
||||
resp = httpx.post(
|
||||
f"{backend_url}/api/internal/etl-completed",
|
||||
json={"pipeline": pipeline},
|
||||
headers={"Authorization": f"Internal-Token {internal_token}"},
|
||||
timeout=300,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
self.logger.info("ETL 完成回调成功: %s", resp.json())
|
||||
else:
|
||||
self.logger.warning("ETL 完成回调失败: status=%d, body=%s", resp.status_code, resp.text)
|
||||
except Exception:
|
||||
self.logger.warning("ETL 完成回调异常(不阻断主流程)", exc_info=True)
|
||||
|
||||
def _run_post_consistency_check(self, timer: EtlTimer) -> str | None:
|
||||
"""ETL 完成后运行数据一致性检查,输出黑盒测试报告。
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import psycopg2
|
||||
|
||||
from api.recording_client import RecordingAPIClient
|
||||
from api.local_json_client import LocalJsonClient
|
||||
from orchestration.cursor_manager import CursorManager
|
||||
@@ -91,6 +93,17 @@ class TaskExecutor:
|
||||
self.logger.info("开始运行任务: %s, run_uuid=%s", task_codes, run_uuid)
|
||||
|
||||
for task_code in task_codes:
|
||||
# 每个任务前检测连接状态,断线自动重连
|
||||
if not self.db_ops.ensure_open():
|
||||
self.logger.error("数据库连接不可用且重连失败,跳过任务 %s", task_code)
|
||||
results.append({
|
||||
"task_code": task_code,
|
||||
"status": "失败",
|
||||
"error": "数据库连接不可用",
|
||||
"counts": {},
|
||||
})
|
||||
continue
|
||||
|
||||
# 为每个任务创建独立的日志缓冲区,避免多任务日志交叉
|
||||
task_log_buf = TaskLogBuffer(task_code, self.logger)
|
||||
try:
|
||||
@@ -113,9 +126,13 @@ class TaskExecutor:
|
||||
task_log_buf.error("任务失败: %s", exc)
|
||||
# CHANGE 2026-02-24 | 任务失败后 rollback,防止 InFailedSqlTransaction 级联
|
||||
try:
|
||||
self.db.rollback()
|
||||
self.db_ops.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
# 连接级异常:尝试重连,避免后续任务全部级联失败
|
||||
if isinstance(exc, (psycopg2.OperationalError, psycopg2.InterfaceError)):
|
||||
self.logger.warning("检测到连接异常,尝试重连...")
|
||||
self.db_ops.ensure_open()
|
||||
results.append({
|
||||
"task_code": task_code,
|
||||
"status": "失败",
|
||||
|
||||
@@ -43,6 +43,7 @@ from tasks.dws import (
|
||||
FinanceRechargeTask,
|
||||
FinanceIncomeStructureTask,
|
||||
FinanceDiscountDetailTask,
|
||||
FinanceAreaDailyTask,
|
||||
# 库存汇总任务
|
||||
GoodsStockDailyTask,
|
||||
GoodsStockWeeklyTask,
|
||||
@@ -167,6 +168,7 @@ default_registry.register("DWS_FINANCE_DAILY", FinanceDailyTask, layer="DWS")
|
||||
default_registry.register("DWS_FINANCE_RECHARGE", FinanceRechargeTask, layer="DWS")
|
||||
default_registry.register("DWS_FINANCE_INCOME_STRUCTURE", FinanceIncomeStructureTask, layer="DWS")
|
||||
default_registry.register("DWS_FINANCE_DISCOUNT_DETAIL", FinanceDiscountDetailTask, layer="DWS")
|
||||
default_registry.register("DWS_FINANCE_AREA_DAILY", FinanceAreaDailyTask, layer="DWS", depends_on=["DWS_FINANCE_DAILY"])
|
||||
# CHANGE [2026-03-27] intent: 移除对 DWD_LOAD_FROM_ODS 的显式依赖,dwd_dws Flow 下该依赖不在批次内只产生无意义 warning
|
||||
default_registry.register("DWS_GOODS_STOCK_DAILY", GoodsStockDailyTask, layer="DWS")
|
||||
default_registry.register("DWS_GOODS_STOCK_WEEKLY", GoodsStockWeeklyTask, layer="DWS")
|
||||
@@ -182,6 +184,7 @@ default_registry.register("DWS_MAINTENANCE", DwsMaintenanceTask, layer="DWS", de
|
||||
"DWS_ASSISTANT_PROJECT_TAG", "DWS_MEMBER_PROJECT_TAG",
|
||||
"DWS_FINANCE_DAILY", "DWS_FINANCE_RECHARGE",
|
||||
"DWS_FINANCE_INCOME_STRUCTURE", "DWS_FINANCE_DISCOUNT_DETAIL",
|
||||
"DWS_FINANCE_AREA_DAILY",
|
||||
"DWS_BUILD_ORDER_SUMMARY",
|
||||
"DWS_GOODS_STOCK_DAILY", "DWS_GOODS_STOCK_WEEKLY", "DWS_GOODS_STOCK_MONTHLY",
|
||||
])
|
||||
|
||||
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"tzdata>=2023.0",
|
||||
"python-dotenv",
|
||||
"openpyxl>=3.1.0",
|
||||
"httpx>=0.24.0",
|
||||
"neozqyy-shared",
|
||||
]
|
||||
|
||||
|
||||
@@ -373,6 +373,8 @@ def transform_area_daily(
|
||||
)
|
||||
# 收集所有涉及的日期
|
||||
all_dates: set[date] = set()
|
||||
# 未知区域名称计数(汇总后一次性输出,避免逐行 warning 产生大量日志噪音)
|
||||
_unknown_area_counts: Dict[str, int] = defaultdict(int)
|
||||
|
||||
for row in settlement_rows:
|
||||
sd = row.get("stat_date")
|
||||
@@ -389,11 +391,7 @@ def transform_area_daily(
|
||||
area_code = resolve_area_code(area_name)
|
||||
|
||||
if area_code is None:
|
||||
# 未知区域:记录警告,不计入具体区域行,但仍计入 all 行
|
||||
logger.warning(
|
||||
"DWS_FINANCE_AREA_DAILY: 未知区域名称 '%s',不计入具体区域",
|
||||
area_name,
|
||||
)
|
||||
_unknown_area_counts[str(area_name)] += 1
|
||||
|
||||
# 提取金额
|
||||
table_fee = safe_decimal_fn(row.get("table_fee_amount", 0))
|
||||
@@ -464,6 +462,15 @@ def transform_area_daily(
|
||||
for k, v in fields.items():
|
||||
bucket[k] = bucket[k] + v
|
||||
|
||||
# 汇总输出未知区域名称(避免逐行 warning 刷屏)
|
||||
if _unknown_area_counts:
|
||||
summary = ", ".join(f"'{k}': {v}次" for k, v in _unknown_area_counts.items())
|
||||
logger.warning(
|
||||
"DWS_FINANCE_AREA_DAILY: 共 %d 条结算单区域未匹配(不计入具体区域,仅计入 all): %s",
|
||||
sum(_unknown_area_counts.values()),
|
||||
summary,
|
||||
)
|
||||
|
||||
# 也收集 global_summary 中的日期
|
||||
for sd in global_index:
|
||||
all_dates.add(sd)
|
||||
|
||||
@@ -104,10 +104,10 @@ class FinanceBaseTask(BaseDwsTask):
|
||||
SUM(CASE WHEN is_first = 1 THEN pay_amount + point_amount ELSE 0 END) AS first_recharge_total,
|
||||
SUM(CASE WHEN is_first = 1 THEN pay_amount ELSE 0 END) AS first_recharge_cash,
|
||||
SUM(CASE WHEN is_first = 1 THEN point_amount ELSE 0 END) AS first_recharge_gift,
|
||||
COUNT(CASE WHEN is_first = 0 OR is_first IS NULL THEN 1 END) AS renewal_count,
|
||||
SUM(CASE WHEN is_first = 0 OR is_first IS NULL THEN pay_amount + point_amount ELSE 0 END) AS renewal_total,
|
||||
SUM(CASE WHEN is_first = 0 OR is_first IS NULL THEN pay_amount ELSE 0 END) AS renewal_cash,
|
||||
SUM(CASE WHEN is_first = 0 OR is_first IS NULL THEN point_amount ELSE 0 END) AS renewal_gift,
|
||||
COUNT(CASE WHEN is_first = 2 THEN 1 END) AS renewal_count,
|
||||
SUM(CASE WHEN is_first = 2 THEN pay_amount + point_amount ELSE 0 END) AS renewal_total,
|
||||
SUM(CASE WHEN is_first = 2 THEN pay_amount ELSE 0 END) AS renewal_cash,
|
||||
SUM(CASE WHEN is_first = 2 THEN point_amount ELSE 0 END) AS renewal_gift,
|
||||
COUNT(DISTINCT member_id) AS recharge_member_count
|
||||
FROM dwd.dwd_recharge_order
|
||||
WHERE site_id = %s
|
||||
|
||||
@@ -139,10 +139,10 @@ class FinanceRechargeTask(FinanceBaseTask):
|
||||
SUM(CASE WHEN is_first = 1 THEN pay_amount ELSE 0 END) AS first_recharge_cash,
|
||||
SUM(CASE WHEN is_first = 1 THEN point_amount ELSE 0 END) AS first_recharge_gift,
|
||||
SUM(CASE WHEN is_first = 1 THEN pay_amount + point_amount ELSE 0 END) AS first_recharge_total,
|
||||
COUNT(CASE WHEN is_first != 1 OR is_first IS NULL THEN 1 END) AS renewal_count,
|
||||
SUM(CASE WHEN is_first != 1 OR is_first IS NULL THEN pay_amount ELSE 0 END) AS renewal_cash,
|
||||
SUM(CASE WHEN is_first != 1 OR is_first IS NULL THEN point_amount ELSE 0 END) AS renewal_gift,
|
||||
SUM(CASE WHEN is_first != 1 OR is_first IS NULL THEN pay_amount + point_amount ELSE 0 END) AS renewal_total,
|
||||
COUNT(CASE WHEN is_first = 2 THEN 1 END) AS renewal_count,
|
||||
SUM(CASE WHEN is_first = 2 THEN pay_amount ELSE 0 END) AS renewal_cash,
|
||||
SUM(CASE WHEN is_first = 2 THEN point_amount ELSE 0 END) AS renewal_gift,
|
||||
SUM(CASE WHEN is_first = 2 THEN pay_amount + point_amount ELSE 0 END) AS renewal_total,
|
||||
COUNT(DISTINCT member_id) AS recharge_member_count,
|
||||
COUNT(DISTINCT CASE WHEN is_first = 1 THEN member_id END) AS new_member_count
|
||||
FROM dwd.dwd_recharge_order
|
||||
@@ -198,20 +198,19 @@ class FinanceRechargeTask(FinanceBaseTask):
|
||||
CASH_CARD_TYPE_ID = 2793249295533893
|
||||
GIFT_CARD_TYPE_IDS = [2791990152417157, 2793266846533445, 2794699703437125]
|
||||
|
||||
# CHANGE 2026-02-21 | dim_member_card_account 无 site_id 字段,改用 register_site_id
|
||||
# CHANGE 2026-02-22 | 需求 B:通过事实表反查,支持跨店消费会员
|
||||
# CHANGE 2026-04-08 | 修复卡余额快照失真:改为门店全量会员
|
||||
# 原逻辑通过 dwd_recharge_order 反查,遗漏"有卡无充值"会员(赠卡、活动等)
|
||||
# 余额代表门店预收负债,应覆盖所有持卡会员
|
||||
sql = """
|
||||
SELECT card_type_id, SUM(balance) AS total_balance
|
||||
FROM dwd.dim_member_card_account
|
||||
WHERE tenant_member_id IN (
|
||||
SELECT DISTINCT member_id
|
||||
FROM dwd.dwd_recharge_order
|
||||
WHERE site_id = %s
|
||||
AND member_id IS NOT NULL
|
||||
AND member_id != 0
|
||||
) AND scd2_is_current = 1
|
||||
AND COALESCE(is_delete, 0) = 0
|
||||
GROUP BY card_type_id
|
||||
SELECT dca.card_type_id, SUM(dca.balance) AS total_balance
|
||||
FROM dwd.dim_member_card_account dca
|
||||
JOIN dwd.dim_member m
|
||||
ON dca.tenant_member_id = m.member_id
|
||||
AND m.scd2_is_current = 1
|
||||
WHERE m.register_site_id = %s
|
||||
AND dca.scd2_is_current = 1
|
||||
AND COALESCE(dca.is_delete, 0) = 0
|
||||
GROUP BY dca.card_type_id
|
||||
"""
|
||||
rows = self.db.query(sql, (site_id,))
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class MemberActivityData:
|
||||
days_since_last_recharge: Optional[int] = None
|
||||
|
||||
visits_14d: int = 0
|
||||
visits_30d: int = 0
|
||||
visits_60d: int = 0
|
||||
visits_total: int = 0
|
||||
|
||||
@@ -451,8 +452,9 @@ class MemberIndexBaseTask(BaseIndexTask):
|
||||
last_visit_time = max(r.get('last_visit_time') for r in day_rows_sorted)
|
||||
data.last_visit_time = last_visit_time
|
||||
|
||||
# 近14/60天到店次数
|
||||
# 近14/30/60天到店次数
|
||||
days_14_ago = base_date - timedelta(days=14)
|
||||
days_30_ago_visit = base_date - timedelta(days=30)
|
||||
days_60_ago = base_date - timedelta(days=60)
|
||||
for r in day_rows_sorted:
|
||||
visit_date = r.get('visit_date')
|
||||
@@ -460,6 +462,8 @@ class MemberIndexBaseTask(BaseIndexTask):
|
||||
continue
|
||||
if visit_date >= days_14_ago:
|
||||
data.visits_14d += 1
|
||||
if visit_date >= days_30_ago_visit:
|
||||
data.visits_30d += 1
|
||||
if visit_date >= days_60_ago:
|
||||
data.visits_60d += 1
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ class NewconvIndexTask(MemberIndexBaseTask):
|
||||
status, segment,
|
||||
member_create_time, first_visit_time, last_visit_time, last_recharge_time,
|
||||
t_v, t_r, t_a,
|
||||
visits_14d, visits_60d, visits_total,
|
||||
visits_14d, visits_30d, visits_60d, visits_total,
|
||||
spend_30d, spend_180d, sv_balance, recharge_60d_amt,
|
||||
interval_count,
|
||||
need_new, salvage_new, recharge_new, value_new,
|
||||
@@ -331,7 +331,7 @@ class NewconvIndexTask(MemberIndexBaseTask):
|
||||
%s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s, %s, %s,
|
||||
%s, %s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s,
|
||||
%s, %s, %s, %s,
|
||||
@@ -352,7 +352,7 @@ class NewconvIndexTask(MemberIndexBaseTask):
|
||||
data.status, data.segment,
|
||||
activity.member_create_time, activity.first_visit_time, activity.last_visit_time, activity.last_recharge_time,
|
||||
activity.t_v, activity.t_r, activity.t_a,
|
||||
activity.visits_14d, activity.visits_60d, activity.visits_total,
|
||||
activity.visits_14d, activity.visits_30d, activity.visits_60d, activity.visits_total,
|
||||
activity.spend_30d, activity.spend_180d, activity.sv_balance, activity.recharge_60d_amt,
|
||||
activity.interval_count,
|
||||
data.need_new, data.salvage_new, data.recharge_new, data.value_new,
|
||||
|
||||
@@ -353,7 +353,12 @@ class SpendingPowerIndexTask(BaseIndexTask):
|
||||
COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id)
|
||||
AS canonical_member_id,
|
||||
s.pay_time,
|
||||
COALESCE(s.pay_amount, 0) AS pay_amount
|
||||
-- DWD 规则 #1:禁止 consume_money/pay_amount,使用 items_sum
|
||||
(COALESCE(s.table_charge_money, 0)
|
||||
+ COALESCE(s.goods_money, 0)
|
||||
+ COALESCE(s.assistant_pd_money, 0)
|
||||
+ COALESCE(s.assistant_cx_money, 0)
|
||||
+ COALESCE(s.electricity_money, 0)) AS items_sum
|
||||
FROM dwd.dwd_settlement_head s
|
||||
LEFT JOIN dwd.dim_member_card_account mca
|
||||
ON s.member_card_account_id = mca.member_card_id
|
||||
@@ -368,14 +373,14 @@ class SpendingPowerIndexTask(BaseIndexTask):
|
||||
SELECT
|
||||
canonical_member_id AS member_id,
|
||||
-- 90 天窗口
|
||||
SUM(pay_amount) AS spend_90,
|
||||
SUM(items_sum) AS spend_90,
|
||||
COUNT(*) AS orders_90,
|
||||
COUNT(DISTINCT {biz_expr}) AS visit_days_90,
|
||||
COUNT(DISTINCT EXTRACT(ISOYEAR FROM pay_time)::int * 100
|
||||
+ EXTRACT(WEEK FROM pay_time)::int) AS active_weeks_90,
|
||||
-- 30 天窗口(子集过滤)
|
||||
SUM(CASE WHEN pay_time >= %s - INTERVAL '{short_days} days'
|
||||
THEN pay_amount ELSE 0 END) AS spend_30,
|
||||
THEN items_sum ELSE 0 END) AS spend_30,
|
||||
SUM(CASE WHEN pay_time >= %s - INTERVAL '{short_days} days'
|
||||
THEN 1 ELSE 0 END) AS orders_30,
|
||||
COUNT(DISTINCT CASE WHEN pay_time >= %s - INTERVAL '{short_days} days'
|
||||
@@ -491,7 +496,12 @@ class SpendingPowerIndexTask(BaseIndexTask):
|
||||
COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id)
|
||||
AS canonical_member_id,
|
||||
{biz_expr_s} AS pay_date,
|
||||
COALESCE(s.pay_amount, 0) AS pay_amount
|
||||
-- DWD 规则 #1:使用 items_sum
|
||||
(COALESCE(s.table_charge_money, 0)
|
||||
+ COALESCE(s.goods_money, 0)
|
||||
+ COALESCE(s.assistant_pd_money, 0)
|
||||
+ COALESCE(s.assistant_cx_money, 0)
|
||||
+ COALESCE(s.electricity_money, 0)) AS items_sum
|
||||
FROM dwd.dwd_settlement_head s
|
||||
LEFT JOIN dwd.dim_member_card_account mca
|
||||
ON s.member_card_account_id = mca.member_card_id
|
||||
@@ -502,7 +512,7 @@ class SpendingPowerIndexTask(BaseIndexTask):
|
||||
AND s.settle_type IN (1, 3)
|
||||
AND s.pay_time >= NOW() - INTERVAL '{long_days} days'
|
||||
)
|
||||
SELECT pay_date, SUM(pay_amount) AS daily_spend
|
||||
SELECT pay_date, SUM(items_sum) AS daily_spend
|
||||
FROM consume_source
|
||||
WHERE canonical_member_id = %s
|
||||
GROUP BY pay_date
|
||||
@@ -544,7 +554,12 @@ class SpendingPowerIndexTask(BaseIndexTask):
|
||||
COALESCE(NULLIF(s.member_id, 0), mca.tenant_member_id)
|
||||
AS canonical_member_id,
|
||||
{biz_expr_s} AS pay_date,
|
||||
COALESCE(s.pay_amount, 0) AS pay_amount
|
||||
-- DWD 规则 #1:使用 items_sum
|
||||
(COALESCE(s.table_charge_money, 0)
|
||||
+ COALESCE(s.goods_money, 0)
|
||||
+ COALESCE(s.assistant_pd_money, 0)
|
||||
+ COALESCE(s.assistant_cx_money, 0)
|
||||
+ COALESCE(s.electricity_money, 0)) AS items_sum
|
||||
FROM dwd.dwd_settlement_head s
|
||||
LEFT JOIN dwd.dim_member_card_account mca
|
||||
ON s.member_card_account_id = mca.member_card_id
|
||||
@@ -558,7 +573,7 @@ class SpendingPowerIndexTask(BaseIndexTask):
|
||||
)
|
||||
SELECT canonical_member_id AS member_id,
|
||||
pay_date,
|
||||
SUM(pay_amount) AS daily_spend
|
||||
SUM(items_sum) AS daily_spend
|
||||
FROM consume_source
|
||||
WHERE canonical_member_id > 0
|
||||
GROUP BY canonical_member_id, pay_date
|
||||
|
||||
@@ -369,7 +369,7 @@ class WinbackIndexTask(MemberIndexBaseTask):
|
||||
status, segment,
|
||||
member_create_time, first_visit_time, last_visit_time, last_recharge_time,
|
||||
t_v, t_r, t_a,
|
||||
visits_14d, visits_60d, visits_total,
|
||||
visits_14d, visits_30d, visits_60d, visits_total,
|
||||
spend_30d, spend_180d, sv_balance, recharge_60d_amt,
|
||||
interval_count,
|
||||
overdue_old, overdue_cdf_p, drop_old, recharge_old, value_old,
|
||||
@@ -382,7 +382,7 @@ class WinbackIndexTask(MemberIndexBaseTask):
|
||||
%s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s, %s, %s,
|
||||
%s, %s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s, %s, %s, %s,
|
||||
%s,
|
||||
%s, %s, %s, %s, %s,
|
||||
@@ -402,7 +402,7 @@ class WinbackIndexTask(MemberIndexBaseTask):
|
||||
data.status, data.segment,
|
||||
activity.member_create_time, activity.first_visit_time, activity.last_visit_time, activity.last_recharge_time,
|
||||
activity.t_v, activity.t_r, activity.t_a,
|
||||
activity.visits_14d, activity.visits_60d, activity.visits_total,
|
||||
activity.visits_14d, activity.visits_30d, activity.visits_60d, activity.visits_total,
|
||||
activity.spend_30d, activity.spend_180d, activity.sv_balance, activity.recharge_60d_amt,
|
||||
activity.interval_count,
|
||||
data.overdue_old, data.overdue_cdf_p, data.drop_old, data.recharge_old, data.value_old,
|
||||
|
||||
Reference in New Issue
Block a user