feat: 2026-04-15~04-20 累积变更基线 — 多主线合流
主线 1: rns1-customer-coach-api + 04-miniapp-core-business 后端实施
- 新增 GET /xcx/coaches/{id}/banner 轻量接口
- performance/records 加 coach_id 参数 + view_board_coach 权限分流
- coach/customer/performance/board/task 服务层重构
- fdw_queries 结算单粒度聚合 + consumption_summary 视图统一
- task_generator 回访宽限 72h + UPSERT 替代策略 + Step 5 保底清理
- recall_detector settle_type=3 双重限制 + 门店级 resolved
主线 2: 小程序权限分流 + 新增 coach-service-records 管理者视角业绩明细页
- perf-progress 共享模块去重 task-list/coach-detail 动画逻辑
- isScattered 散客标记端到端
- foodDetail/phoneFull/creator* 字段透传
主线 3: P19 指数回测框架 Phase 1+2
- 3 个指数表 stat_date 日快照模式
- 新增 DWS_INDEX_BACKFILL / DWS_TASK_SIMULATION 工具任务
- task_engine 升级 HTTP 实时 + 推演回测双模式
主线 4: Core 维度层启用
- 新增 CORE_DIM_SYNC 任务(DWD → core 4 维度表)
- 修复 app 视图空查询问题
主线 5: member_project_tag 改为 LAST_30_VISITS 消费次数窗口
主线 6: 2 个迁移 SQL 已执行(stat_date + member_project_tag 新窗口)
- schema 基线与 DDL 快照同步
主线 7: 开发机路径迁移 C:\NeoZQYY → C:\Project\NeoZQYY(约 95% 改动量)
附带: 新建运维脚本(churned_customer_report / simulate_historical_tasks /
backfill_index_snapshots)+ tools/task-analysis/ 任务分析工具
合计 157 文件。未包含中间产物(tmp/ .playwright-mcp/ inspect-* excel/sheet 分析 txt)。
审计记录见下一个 commit。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,7 +147,7 @@ _DEFAULT_PARAMS: dict[str, float] = {
|
||||
"transfer_score_w_ms": 0.3,
|
||||
"transfer_score_w_ml": 0.2,
|
||||
"max_transfer_count": 4,
|
||||
"follow_up_visit_retention_hours": 48,
|
||||
"follow_up_visit_retention_hours": 72,
|
||||
# CHANGE 2026-03-29 | OS 分级分配:升级倍数参数
|
||||
"escalation_comanage_multiplier": 2.5,
|
||||
"escalation_pool_multiplier": 4.0,
|
||||
@@ -554,15 +554,22 @@ def _process_pair(
|
||||
stats["skipped"] += 1
|
||||
return
|
||||
|
||||
# Case B: 不同类型的 active 任务 → 关闭旧任务 + 创建新任务
|
||||
for task_id, old_type, old_expires_at, old_created_at in existing_tasks:
|
||||
if should_replace_task(old_type, new_task_type):
|
||||
# follow_up_visit 被高优先级任务顶替时,填充 expires_at 而非直接 inactive
|
||||
if old_type == "follow_up_visit" and old_expires_at is None:
|
||||
# Case B: 不同类型的 active 任务 → 混合策略
|
||||
# - follow_up_visit 被替代:保留宽限期(填 expires_at 72h)+ 新建高优先任务
|
||||
# - 其他类型被替代:原地覆盖(UPDATE task_type + priority_score)
|
||||
overridden = False
|
||||
need_create_new = False
|
||||
for i, (task_id, old_type, old_expires_at, old_created_at) in enumerate(existing_tasks):
|
||||
if not should_replace_task(old_type, new_task_type):
|
||||
continue
|
||||
|
||||
if old_type == "follow_up_visit":
|
||||
# follow_up_visit 特殊处理:填充 72h 宽限期,不关闭
|
||||
if old_expires_at is None:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.coach_tasks
|
||||
SET expires_at = created_at + INTERVAL '48 hours',
|
||||
SET expires_at = created_at + INTERVAL '72 hours',
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
@@ -578,60 +585,86 @@ def _process_pair(
|
||||
new_task_type=old_type,
|
||||
detail={"reason": "higher_priority_task_created"},
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.coach_tasks
|
||||
SET status = 'inactive', updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(task_id,),
|
||||
)
|
||||
_insert_history(
|
||||
cur,
|
||||
task_id,
|
||||
action="type_change_close",
|
||||
old_status="active",
|
||||
new_status="inactive",
|
||||
old_task_type=old_type,
|
||||
new_task_type=new_task_type,
|
||||
)
|
||||
|
||||
need_create_new = True
|
||||
stats["replaced"] += 1
|
||||
elif not overridden:
|
||||
# 非 follow_up:原地覆盖
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.coach_tasks
|
||||
SET task_type = %s, priority_score = %s, updated_at = NOW()
|
||||
WHERE id = %s AND status = 'active'
|
||||
""",
|
||||
(new_task_type, float(priority_score), task_id),
|
||||
)
|
||||
_insert_history(
|
||||
cur,
|
||||
task_id,
|
||||
action="type_override",
|
||||
old_status="active",
|
||||
new_status="active",
|
||||
old_task_type=old_type,
|
||||
new_task_type=new_task_type,
|
||||
detail={"old_priority": float(priority_score)},
|
||||
)
|
||||
overridden = True
|
||||
stats["replaced"] += 1
|
||||
else:
|
||||
# 多余的同对任务:关闭
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.coach_tasks
|
||||
SET status = 'inactive', updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
(task_id,),
|
||||
)
|
||||
_insert_history(
|
||||
cur,
|
||||
task_id,
|
||||
action="type_change_close",
|
||||
old_status="active",
|
||||
new_status="inactive",
|
||||
old_task_type=old_type,
|
||||
new_task_type=new_task_type,
|
||||
)
|
||||
|
||||
# ── 创建新任务 ──
|
||||
expires_at_val = None
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.coach_tasks
|
||||
(site_id, assistant_id, member_id, task_type, status,
|
||||
priority_score, expires_at, parent_task_id)
|
||||
VALUES (%s, %s, %s, %s, 'active', %s, %s, %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
site_id,
|
||||
assistant_id,
|
||||
member_id,
|
||||
new_task_type,
|
||||
float(priority_score),
|
||||
expires_at_val,
|
||||
existing_tasks[0][0] if existing_tasks else None,
|
||||
),
|
||||
)
|
||||
new_task_id = cur.fetchone()[0]
|
||||
|
||||
_insert_history(
|
||||
cur,
|
||||
new_task_id,
|
||||
action="created",
|
||||
old_status=None,
|
||||
new_status="active",
|
||||
old_task_type=existing_tasks[0][1] if existing_tasks else None,
|
||||
new_task_type=new_task_type,
|
||||
)
|
||||
|
||||
stats["created"] += 1
|
||||
# 需要新建任务的场景:
|
||||
# 1. follow_up_visit 被替代(宽限期保留原任务,需新建高优先任务)
|
||||
# 2. 没有可覆盖的非 follow_up 任务
|
||||
if need_create_new or not overridden:
|
||||
# upsert:若同类型 active 已存在(recall_detector 先行创建)则更新 priority
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO biz.coach_tasks
|
||||
(site_id, assistant_id, member_id, task_type, status,
|
||||
priority_score, expires_at, parent_task_id)
|
||||
VALUES (%s, %s, %s, %s, 'active', %s, %s, %s)
|
||||
ON CONFLICT (site_id, assistant_id, member_id, task_type) WHERE (status = 'active')
|
||||
DO UPDATE SET priority_score = EXCLUDED.priority_score, updated_at = NOW()
|
||||
RETURNING id
|
||||
""",
|
||||
(
|
||||
site_id,
|
||||
assistant_id,
|
||||
member_id,
|
||||
new_task_type,
|
||||
float(priority_score),
|
||||
None,
|
||||
existing_tasks[0][0] if existing_tasks else None,
|
||||
),
|
||||
)
|
||||
new_task_id = cur.fetchone()[0]
|
||||
_insert_history(
|
||||
cur,
|
||||
new_task_id,
|
||||
action="created",
|
||||
old_status=None,
|
||||
new_status="active",
|
||||
old_task_type=existing_tasks[0][1] if existing_tasks else None,
|
||||
new_task_type=new_task_type,
|
||||
)
|
||||
stats["created"] += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
@@ -1145,6 +1178,45 @@ def _generate_baseline_relationship_tasks(
|
||||
pair["member_id"],
|
||||
)
|
||||
biz_conn.rollback()
|
||||
|
||||
# Step 5: 反向清理 -- 关闭不再符合条件的 relationship_building 任务
|
||||
# 对已有 active relationship_building 但不在 get_all_service_pairs 结果中的对,关闭
|
||||
valid_pairs = {(p["assistant_id"], p["member_id"]) for p in all_pairs}
|
||||
stale_closed = 0
|
||||
try:
|
||||
with biz_conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, assistant_id, member_id
|
||||
FROM biz.coach_tasks
|
||||
WHERE site_id = %s
|
||||
AND task_type = 'relationship_building'
|
||||
AND status = 'active'
|
||||
""",
|
||||
(site_id,),
|
||||
)
|
||||
for task_id, aid, mid in cur.fetchall():
|
||||
if (aid, mid) not in valid_pairs:
|
||||
cur.execute(
|
||||
"UPDATE biz.coach_tasks SET status = 'inactive', updated_at = NOW() WHERE id = %s",
|
||||
(task_id,),
|
||||
)
|
||||
_insert_history(
|
||||
cur, task_id,
|
||||
action="pool_cleanup",
|
||||
old_status="active",
|
||||
new_status="inactive",
|
||||
old_task_type="relationship_building",
|
||||
new_task_type="relationship_building",
|
||||
detail={"reason": "pair_no_longer_qualifies"},
|
||||
)
|
||||
stale_closed += 1
|
||||
biz_conn.commit()
|
||||
if stale_closed:
|
||||
logger.info("保底任务清理: site_id=%s, 关闭 %d 个不再符合条件的 relationship_building", site_id, stale_closed)
|
||||
except Exception:
|
||||
logger.exception("保底任务清理失败: site_id=%s", site_id)
|
||||
biz_conn.rollback()
|
||||
finally:
|
||||
biz_conn.close()
|
||||
|
||||
@@ -1158,7 +1230,7 @@ def _handle_no_task_condition(
|
||||
) -> None:
|
||||
"""
|
||||
当不满足任何任务生成条件时:
|
||||
1. follow_up_visit → 填充 expires_at = created_at + 48h
|
||||
1. follow_up_visit → 填充 expires_at = created_at + 72h
|
||||
2. high_priority_recall / priority_recall → 直接关闭(inactive)
|
||||
|
||||
CHANGE 2026-03-24 | Prompt: 修复召回任务不自动关闭 bug |
|
||||
@@ -1185,7 +1257,7 @@ def _handle_no_task_condition(
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE biz.coach_tasks
|
||||
SET expires_at = created_at + INTERVAL '48 hours',
|
||||
SET expires_at = created_at + INTERVAL '72 hours',
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
|
||||
Reference in New Issue
Block a user