chore: #86 清理废弃接口 + alembic 存量 lint
All checks were successful
gitleaks / gitleaks (push) Successful in 6s
backend-test / backend-lint-test (push) Successful in 3m52s
backend-test / frontend-lint-test (push) Successful in 29s

用户 2026-09-14 决策:「废弃的接口 就清掉;lint 该清理清理」。

## 一、清理废弃接口 `GET /analytics/summary`(4 处)

**为什么删而不是继续标"废弃"**:
1. 标记已存在(`analytics.py` 路由 tags = 「已废弃-见analysis-views」),
   但**仍在被周期性预热** → 白白消耗 DB/Redis(`cache_warmer.py:236`)
2. 前端 `web/src/api/analytics.js` 的 `getSummary()` **全仓零调用点**
   (grep 仅命中定义处,无任何 import/使用)
3. ★ **最关键**:其旧公式 = 「工单投诉类 + 315 全表 + 卡商 全表」
   (`analytics.py:1649`)—— **含卡商且不扣重叠**,与 #53/#81 已确立的口径
   **直接冲突**。留着就是**错误口径的残留入口**:将来若有人误用,
   会拿到与看板不一致的投诉数 → 属隐患,非无害死代码。

**删除清单**:
- `backend/app/services/analytics.py`:`dashboard_summary()` 函数本体(142 行)
- `backend/app/api/v1/analytics.py`:`GET /summary` 路由
- `backend/app/services/cache_warmer.py`:预热项 `("summary", ...)`
- `web/src/api/analytics.js`:`getSummary()`
(均在删除处留注释说明原因,便于后人检索)

## 二、alembic 存量 lint 清零(63 项 → 0)

```
清理前: 57 I001(import 未排序) + 5 F401(未使用) + 1 W291(尾随空格) = 63
清理后: All checks passed!
```
- 62 项 `ruff check --fix` 自动修复(**纯 import 排序,零逻辑变更**)
- 1 项 W291 手动修正(`d9b7f1b89c1d_...py:4` `Revises:` 尾随空格)

**项目门禁升级**:`ruff check app/ tests/` → **`ruff check app/ tests/ alembic/`**
(此前 alembic 不在门禁范围,故存量问题长期累积)

## 三、安全校验(删除操作必须的谨慎)

- **删除范围精确**:`diff` 备份确认**仅** `dashboard_summary` 被删
- 过程中**误删了紧随其后的 `_CATEGORY_BUCKETS` 常量**(初版删多了 8 行),
  被 `ruff F821` 立即发现并已恢复 → 说明 lint 既是清理对象也是**安全网**
- `alembic heads` 仍为**单 head**(`c4d5e6f7a8b9`)→ 迁移链未被改坏

## 门禁
```
pytest                   1112 passed, 42 skipped(与清理前一致,零回归)
ruff app/ tests/ alembic/ All checks passed!
eslint                   0 error
build                    ✓
```

## 遗留(已在 #86 登记,非本单范围)
- `docs/审查行动项登记.md` / `docs/arch/*` 中仍有 `dashboard_summary` 的**历史记述**
  (属文档沿革,保留不动;如需追溯可查本单)

工单 #86
This commit is contained in:
2026-09-14 09:29:41 +08:00
parent c361e73a29
commit 3aac9ee38b
61 changed files with 84 additions and 214 deletions

View File

@@ -1,12 +1,11 @@
"""Alembic 环境配置"""
import logging
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from alembic import context
from app.core.config import settings
from app.core.database import Base
from app.models import * # noqa: F401,F403 # 注册所有模型

View File

@@ -6,10 +6,11 @@ Create Date: 2026-09-09 10:45:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = '0f822d39bb6e'
down_revision: Union[str, None] = 'e0f1a2b3c4d5'
branch_labels: Union[str, Sequence[str], None] = None

View File

@@ -7,9 +7,9 @@ Create Date: 2026-07-26 14:50:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = '11bcdc63728f'

View File

@@ -7,8 +7,6 @@ Create Date: 2026-08-20 08:56:28.292288
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = '64a584531400'
down_revision: Union[str, None] = 'r6f7a8b9c0d1'

View File

@@ -6,9 +6,10 @@ Create Date: 2026-08-25 11:06:57.088116
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = '73a7922ec598'
down_revision: Union[str, None] = 'm7n8o9p0q1r2'
branch_labels: Union[str, Sequence[str], None] = None

View File

@@ -9,10 +9,11 @@ Create Date: 2026-08-07 13:06:50.653313
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = '97862042a459'
down_revision: Union[str, None] = 'c3e4f5a6b7c8'
branch_labels: Union[str, Sequence[str], None] = None

View File

@@ -17,10 +17,9 @@ Create Date: 2026-08-18 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'a0b1c2d3e4f5'
down_revision: Union[str, None] = 'p1b2c3d4e5f6'

View File

@@ -10,9 +10,10 @@ Create Date: 2026-08-03 21:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'a1b2c3d4e5f6'
down_revision: Union[str, None] = 'b8c9d0e1f2a3'

View File

@@ -10,10 +10,11 @@ Create Date: 2026-08-05 14:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "a1b2c3d4e5f7"
down_revision: Union[str, None] = "z9y8x7w6v5u4"

View File

@@ -20,10 +20,11 @@ Create Date: 2026-08-09 09:00:00.000000
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'a2c4e6f8b0d2'
down_revision: Union[str, None] = 'z9y8x7w6v5u4'

View File

@@ -10,10 +10,11 @@ Create Date: 2026-07-26 18:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'a3f8c2e91b4d'
down_revision: Union[str, None] = 'd9b7f1b89c1d'
branch_labels: Union[str, Sequence[str], None] = None

View File

@@ -12,7 +12,6 @@ from typing import Sequence, Union
from alembic import op
revision: str = 'a6f7a8b9c1d0'
down_revision: Union[str, None] = 'z5e6f7a8b9c0'
branch_labels: Union[str, Sequence[str], None] = None

View File

@@ -16,9 +16,9 @@ Create Date: 2026-08-03 11:50:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'a7b8c9d0e1f2'

View File

@@ -15,9 +15,9 @@ Create Date: 2026-08-07 23:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'aa11bb22cc33'

View File

@@ -14,10 +14,10 @@ Create Date: 2026-08-18 01:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'b0c1d2e3f4a5'
down_revision: Union[str, None] = 'a0b1c2d3e4f5'

View File

@@ -14,9 +14,9 @@ Create Date: 2026-08-06 14:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b2d3e4f5a6b7'

View File

@@ -10,9 +10,9 @@ Create Date: 2026-07-26 23:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'b4c9d1f02e5a'
down_revision: Union[str, None] = 'a3f8c2e91b4d'

View File

@@ -27,10 +27,11 @@ Create Date: 2026-08-07 14:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'b8c9d0e1f2a3'
down_revision: Union[str, None] = 'a7b8c9d0e1f2'

View File

@@ -16,10 +16,10 @@ Create Date: 2026-08-18 02:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'c0d1e2f3a4b5'
down_revision: Union[str, None] = 'b0c1d2e3f4a5'

View File

@@ -22,10 +22,10 @@ Create Date: 2026-08-06 14:50:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'c3e4f5a6b7c8'

View File

@@ -10,9 +10,9 @@ Create Date: 2026-07-26 23:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'c5d0e2a13f6b'
down_revision: Union[str, None] = 'b4c9d1f02e5a'

View File

@@ -6,10 +6,10 @@ Create Date: 2026-08-22 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = "d5f6a7b8c9d0"
down_revision: Union[str, None] = "n9a8b7c6d5e4f"

View File

@@ -1,15 +1,16 @@
"""init schema with data sources persons and upload archive
Revision ID: d9b7f1b89c1d
Revises:
Revises:
Create Date: 2026-07-26 17:00:28.338446
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'd9b7f1b89c1d'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None

View File

@@ -6,9 +6,9 @@ Create Date: 2026-08-22 13:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = "e1f2a3b4c5d6"
down_revision: Union[str, None] = "d5f6a7b8c9d0"

View File

@@ -9,9 +9,9 @@ Create Date: 2026-07-26 23:50:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'e7a1b3c4d5e6'
down_revision: Union[str, None] = 'c5d0e2a13f6b'

View File

@@ -22,9 +22,10 @@ Create Date: 2026-08-23
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'f0e1d2c3b4a5'
down_revision: Union[str, None] = 'e1f2a3b4c5d6'

View File

@@ -10,9 +10,10 @@ Create Date: 2026-08-23
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'f1e2d3c4b5a6'
down_revision: Union[str, None] = 'f0e1d2c3b4a5'

View File

@@ -10,9 +10,10 @@ Create Date: 2026-08-23
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'f2e3d4c5b6a7'
down_revision: Union[str, None] = 'f1e2d3c4b5a6'

View File

@@ -10,9 +10,9 @@ Create Date: 2026-07-26 20:30:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'f8b2c4d6e7a8'
down_revision: Union[str, None] = 'e7a1b3c4d5e6'

View File

@@ -10,9 +10,10 @@ Create Date: 2026-08-23
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'g1a2b3c4d5e6'
down_revision: Union[str, None] = 'f2e3d4c5b6a7'

View File

@@ -9,10 +9,10 @@ Create Date: 2026-07-26 21:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'g9c3d5e7f9a0'
down_revision: Union[str, None] = 'f8b2c4d6e7a8'

View File

@@ -5,9 +5,9 @@ Revises: 11bcdc63728f
Create Date: 2026-07-27 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = "h1a2b3c4d5e6"
down_revision = "11bcdc63728f"

View File

@@ -10,9 +10,10 @@ Create Date: 2026-08-23
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'h1i2j3k4l5m6'
down_revision: Union[str, None] = 'g1a2b3c4d5e6'

View File

@@ -8,10 +8,10 @@ Create Date: 2026-07-27 16:00:00.000000
- device_combos: 设备套餐导出表(对齐 BMS 设备套餐页导出列)
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "k2b3c4d5e6f7"
down_revision = "h1a2b3c4d5e6"

View File

@@ -14,10 +14,6 @@ Create Date: 2026-08-17 10:00:00.000000
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'm0a1b2c3d4e5'
down_revision: Union[str, Sequence[str], None] = ('a2c4e6f8b0d2', 'aa11bb22cc33')

View File

@@ -10,9 +10,9 @@ Create Date: 2026-07-28 10:00:00.000000
启动时 ensure_data_sources_registered 也会按种子刷新,此处回填保证迁移后立即可用
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = "m3c4d5e6f7a8"
down_revision = "k2b3c4d5e6f7"

View File

@@ -12,9 +12,10 @@ Create Date: 2026-08-24
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'm7n8o9p0q1r2'
down_revision: Union[str, None] = 'h1i2j3k4l5m6'

View File

@@ -26,9 +26,9 @@ Create Date: 2026-08-17 10:30:00.000000
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'n1b2c3d4e5f6'

View File

@@ -10,10 +10,10 @@ Create Date: 2026-07-28 15:00:00.000000
(退款/投诉/接待/绩效),复用现有 24 个预设视图组合
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "n4d5e6f7b8c9"
down_revision = "m3c4d5e6f7a8"

View File

@@ -9,10 +9,10 @@ Create Date: 2026-08-21 16:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'n9a8b7c6d5e4f'
down_revision: Union[str, None] = 'x4a5b6c7d8e9f'

View File

@@ -13,9 +13,9 @@ Create Date: 2026-08-17 20:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'o1b2c3d4e5f6'
down_revision: Union[str, None] = 'n1b2c3d4e5f6'

View File

@@ -13,9 +13,9 @@ Create Date: 2026-08-17 22:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'p1b2c3d4e5f6'
down_revision: Union[str, None] = 'o1b2c3d4e5f6'

View File

@@ -11,10 +11,10 @@ Create Date: 2026-07-29 10:00:00.000000
模块职责三分离: SpecialTask=外部专项执行 / TodoItem=日常待办 / Measure=管理措施(本表)
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "p5e6f7a8b9c0"
down_revision = "n4d5e6f7b8c9"

View File

@@ -13,10 +13,10 @@ Create Date: 2026-07-29 11:00:00.000000
审计→措施闭环: AuditNote.output_measure_id FK→measures(可选)。
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "q6f7a8b9c0d1"
down_revision = "p5e6f7a8b9c0"

View File

@@ -10,9 +10,9 @@ Create Date: 2026-08-19 19:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = "r6f7a8b9c0d1"
down_revision: Union[str, None] = "w1x2y3z4a5b6"

View File

@@ -11,10 +11,10 @@ Create Date: 2026-07-29 17:00:00.000000
- 退款挽回 = cs_records 退款项接待SN − refunds 实际退款SN 的集合差(按设备SN核算)
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "r7a8b9c0d1e2"
down_revision = "q6f7a8b9c0d1"

View File

@@ -12,10 +12,10 @@ Create Date: 2026-07-29 18:00:00.000000
- 数据迁移: framework 旧格式 view_id(单值)→view_ids(列表), related_report_id→related_report_ids
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "s8b9c0d1e2f3"
down_revision = "r7a8b9c0d1e2"

View File

@@ -10,10 +10,10 @@ Create Date: 2026-07-29 19:00:00.000000
- 数据迁移: 10项标准框架 + weekly_section_bindings 合并写入"周报默认模板"种子
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision = "t9c0d1e2f3a4"
down_revision = "s8b9c0d1e2f3"

View File

@@ -14,9 +14,9 @@ Create Date: 2026-07-30 10:00:00.000000
- 生命周期为离散单点事件,挂 persons 标量列,不进 SCD2 表
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = "u0d1e2f3a4b5"
down_revision = "t9c0d1e2f3a4"

View File

@@ -15,7 +15,6 @@ Create Date: 2026-07-30 10:30:00.000000
from alembic import op
revision = "v1a2b3c4d5e6"
down_revision = "u0d1e2f3a4b5"
branch_labels = None

View File

@@ -11,9 +11,9 @@ Create Date: 2026-08-19 10:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = "w1x2y3z4a5b6"
down_revision: Union[str, None] = "c0d1e2f3a4b5"

View File

@@ -8,9 +8,10 @@ Create Date: 2026-07-30
并回填现有 HR 人员 = '企微客服部'(店铺客服后续批量纳入时设店铺客服部)。
"""
from alembic import op
import sqlalchemy as sa
from alembic import op
revision = "w2b3c4d5e6f7"
down_revision = "v1a2b3c4d5e6"
branch_labels = None

View File

@@ -18,6 +18,7 @@ Create Date: 2026-08-20 10:00:00.000000
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
revision: str = 'x2b3c4d5e6f7'

View File

@@ -9,10 +9,10 @@ Create Date: 2026-07-31
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'x3c4d5e6f7a9'
down_revision: Union[str, None] = 'w2b3c4d5e6f7'

View File

@@ -10,10 +10,9 @@ Create Date: 2026-07-31
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
revision: str = 'y4d5e6f7a8b0'
down_revision: Union[str, None] = 'x3c4d5e6f7a9'

View File

@@ -13,9 +13,9 @@ Create Date: 2026-08-01
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from alembic import op
revision: str = 'z5e6f7a8b9c0'
down_revision: Union[str, None] = 'y4d5e6f7a8b0'

View File

@@ -13,10 +13,11 @@ Create Date: 2026-08-04 16:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'z9y8x7w6v5u4'
down_revision: Union[str, None] = 'a1b2c3d4e5f6'

View File

@@ -32,9 +32,14 @@ def _parse_dt(s: str | None) -> datetime | None:
return datetime.fromisoformat(s) if s else None
@router.get("/summary", response_model=R, summary="首页汇总卡片")
def summary(db: DBSession, _: CurrentUser):
return R(data=cached_call("summary", 60, A.dashboard_summary, db))
# ⚠️ 2026-09-14 (#86) 已删除 `GET /analytics/summary`(原 `A.dashboard_summary`):
# · 该接口标记「已废弃-见 analysis-views」,前端 `web/src/api/analytics.js` 的
# `getSummary()` **全仓零调用点**(只有定义无使用)
# · 但其旧公式为「工单投诉类 + 315全表 + 卡商全表」—— **含卡商且不扣重叠**,
# 与 #53/#81 已确立的口径**直接冲突**
# · `cache_warmer._warm_reports` 仍在周期性预热它 → **白白消耗 DB/Redis**
# → 属于「废弃但仍在跑」的死代码,且是**错误口径的残留入口**(留着即隐患:
# 将来若有人误用,会得到与看板不一致的投诉数)。故彻底删除,而非继续标记废弃。
# ---------- 看板1:运营分析 ----------

View File

@@ -1605,148 +1605,6 @@ def operator_refund_stats(db: Session, start: datetime | None = None, end: datet
]
# ---------- 总览卡片 ----------
def dashboard_summary(db: Session) -> dict[str, Any]:
"""首页汇总卡片 【全域统一退款核算 v3: 四路经营出款 + 实际现金流 + 对账偏差】"""
# ── 统一核算: 四路A/B/C/D + 实际现金流E + 对账偏差 ──
routes = _cash_routes_total(db)
a = routes["route_a"] # A.退费单线上直退 (refunds refund_method='线上')
b = routes["route_b"] # B.钱包用户取现 (wallet op_type='用户提现')
c = routes["route_c"] # C.闪猫退款 (独立业务表 shanmao_refunds)
d = routes["route_d"] # D.手动退款 (独立业务表 manual_refunds)
op = routes["operational"] # 经营估算 = A+B+C+D (四路经营出款)
e = routes["actual_cashflow"] # 实际现金流 E = refund_record_reports 退款成功合计 (对账基准,不分C/D桶)
fc = routes["financial"] # 向后兼容: financial = actual_cashflow = E
diff = routes["diff"] # 对账偏差 = 经营 − 实际
# 兼容旧键: "退款总金额/笔数" 严格按路线A (退费单线上直退)
refund_total = float(a["amount"])
refund_count = a["count"]
estimated = op["amount"]
merchant_cash = fc["amount"]
# 钱包余额总计(各账户最新余额之和)
wallet_balance = db.scalar(
select(func.coalesce(func.sum(WalletWithdrawal.wallet_balance), 0))
) or 0
# 客服接待量(智能助手处理记录数)
cs_reception_count = db.scalar(select(func.count()).select_from(CsRecord)) or 0
# ---- 投诉统计 (口径 = service_tickets 投诉 + external_complaints + card_complaints 三表 UNION) ----
# 1. 投诉工单总数 (service_tickets: _ticket_category_case()==投诉; 另叠加两张独立投诉表)
complaint_st = db.scalar(
select(func.count())
.select_from(ServiceTicket)
.where(_ticket_category_case() == "投诉")
) or 0
complaint_ec = 0
complaint_cc = 0
if _INCLUDE_COMPLAINT_FILES:
complaint_ec = db.scalar(select(func.count()).select_from(ExternalComplaint)) or 0
complaint_cc = db.scalar(select(func.count()).select_from(CardComplaint)) or 0
complaint_total = complaint_st + complaint_ec + complaint_cc
# 2. 投诉子类分布 (service_tickets 内部投诉分类 + 外部投诉合并)
complaint_sub_rows = db.execute(
select(_complaint_subitem_case(), func.count().label("cnt"))
.select_from(ServiceTicket)
.where(_ticket_category_case() == "投诉")
.group_by("complaint_sub")
.order_by(text("cnt DESC"))
).all()
complaint_by_sub = [
{"name": r[0], "value": int(r[1] or 0)} for r in complaint_sub_rows
]
# 把 external_complaints 的 complaint_category 归并为 "外部投诉平台" 子类
# (避免与内部投诉子类混淆, 同时累加独立投诉表)
#
# ⚠️ 2026-09-10 (#47): 卡商**单列**为独立子类「(外)卡商」, 不再并入外部。
# 用户要求"卡商投诉 可以单独于外部投诉, 可以单列 (外)卡商 这个分类;
# 在各个统计口 都单列出来"。
if complaint_ec or complaint_cc:
_name_ec = "外部投诉(监督局/315等)"
_name_cc = BUCKET_CARD # "(外)卡商"
_existing_subs = {x["name"] for x in complaint_by_sub}
if complaint_ec:
complaint_by_sub.append({"name": _name_ec, "value": int(complaint_ec)})
if complaint_cc:
complaint_by_sub.append({"name": _name_cc, "value": int(complaint_cc)})
complaint_by_sub.sort(key=lambda x: -x["value"])
# 3. 投诉状态分布 (待处理/已完成/终止) — 仅 service_tickets (两张独立投诉表状态口径不同暂不合并展示,避免饼图失真)
complaint_status_rows = db.execute(
select(
func.coalesce(ServiceTicket.status, "未标注").label("st"),
func.count().label("cnt"),
)
.select_from(ServiceTicket)
.where(_ticket_category_case() == "投诉")
.group_by("st")
.order_by(text("cnt DESC"))
).all()
complaint_by_status = [
{"name": r[0], "value": int(r[1] or 0)} for r in complaint_status_rows
]
# 补齐外部投诉/卡商投诉的状态缺失时,按 result 字段粗略估算 已完成(有效值)/待处理(未标注)
if _INCLUDE_COMPLAINT_FILES and complaint_ec:
ec_done = db.scalar(
select(func.count()).select_from(ExternalComplaint)
.where(func.coalesce(ExternalComplaint.result, "").ilike("%有效%"))
) or 0
complaint_by_status.append({"name": "外部投诉-已处理", "value": int(ec_done)})
if complaint_ec - ec_done > 0:
complaint_by_status.append({"name": "外部投诉-待跟进", "value": int(complaint_ec - ec_done)})
if _INCLUDE_COMPLAINT_FILES and complaint_cc:
cc_done = db.scalar(
select(func.count()).select_from(CardComplaint)
.where(func.coalesce(CardComplaint.result, "").ilike("%完成%"))
) or 0
complaint_by_status.append({"name": "卡商投诉-已处理", "value": int(cc_done)})
if complaint_cc - cc_done > 0:
complaint_by_status.append({"name": "卡商投诉-待跟进", "value": int(complaint_cc - cc_done)})
# 4. 工单总数(非投诉类) — 保持原 service_tickets 全量,仅用于投诉占比分母
ticket_total = db.scalar(select(func.count()).select_from(ServiceTicket)) or 0
return {
# ── 兼容旧键: 保持向后兼容 ──
"refund_total": refund_total, # 退款总金额 = 路线A(退费单线上直退)
"refund_count": refund_count, # 退款总笔数 = 路线A(退费单线上直退)
"cash_out_total": estimated, # 经营估算现金流 = A+B+C+D
"cash_out_direct": a["amount"], # 直接退款 = A
"cash_out_withdraw": b["amount"], # 用户提现 = B
"actual_cashflow": e["amount"], # (新) 实际现金流 E = 退款记录报表退款成功合计
"merchant_cash": merchant_cash, # 兼容旧键: 商户号现金流 = E = actual_cashflow
"cash_diff": diff["amount"], # 对账偏差 = 经营估算 − 实际现金流 (C/D未建=A+B−E)
"wallet_balance_total": float(wallet_balance),
"cs_reception_count": cs_reception_count,
# --- 投诉维度 ---
"ticket_total": ticket_total,
"complaint_total": complaint_total,
"complaint_ratio": round(complaint_total / ticket_total, 4) if ticket_total else 0,
"complaint_by_sub": complaint_by_sub,
"complaint_by_status": complaint_by_status,
# ── 新增: 四路线拆分 + 实际现金流E + 对账偏差 (前端可直接渲染卡片) ──
"cash_routes": {
"route_a": a, # {count, amount} A.退费单线上直退
"route_b": b, # {count, amount} B.钱包用户取现
"route_c": c, # {count, amount} C.闪猫退款 (独立业务表 shanmao_refunds)
"route_d": d, # {count, amount} D.手动退款 (独立业务表 manual_refunds)
"operational": op, # {count, amount} 经营估算 = A+B+C+D
"actual_cashflow": e, # {count, amount} 实际现金流 = E (退款记录报表退款成功,对账基准)
"financial": fc, # {count, amount} 向后兼容: 财务实际 = actual_cashflow = E
"diff": diff, # {count, amount} 对账偏差 = 经营 − 实际
},
# ── 口径说明注释 (前端tooltip可直接显示) ──
"_spec": {
"refund_total": "路线A.退费单线上直退 (refunds WHERE refund_method='线上')",
"cash_out_total": "经营估算(四路经营出款合计) = A线上直退 + B用户取现 + C闪猫退款 + D手动退款",
"actual_cashflow": "实际现金流(对账基准) = refund_record_reports WHERE refund_status IN ('退款成功','成功','SUCCESS') 合计, 不分ORIGINAL/BALANCE桶 (2026-09-05 v1.1 起替代 merchant_refunds)",
"merchant_cash": "实际现金流(对账基准,同actual_cashflow) = 退款记录报表退款成功合计",
"cash_diff": "对账偏差 = 经营估算 − 实际现金流 (商户号现金流不与经营数据累加)",
},
}
# ---------- 看板5: 客服接待看板 (对齐周报4模块) ----------
# 接待分项分类(对齐周报:投诉/退款/其他问题/套餐问题/咨询/综合问题)

View File

@@ -233,7 +233,8 @@ def _warm_reports(db, *, force: bool = False) -> int:
sd, ed = _dt(s), _dt(e)
specs = [
# 静态(无窗口, 任意查看都命中同一 key)
("summary", "summary", A.dashboard_summary, (db,), {}),
# ⚠️ 2026-09-14 (#86): 已移除 `("summary", ...A.dashboard_summary)` ——
# 该接口已删除(废弃 + 错误口径 + 前端零调用),继续预热是纯浪费。
("op:status", "op:status", A.operation_status_dist, (db,), {}),
("op:method", "op:method", A.operation_method_dist, (db,), {}),
("op:reason:10", "op:reason:10", A.operation_reason_top, (db, 10), {}),

View File

@@ -1,9 +1,8 @@
import request from '@/api'
// 首页汇总
export function getSummary() {
return request.get('/analytics/summary')
}
// ⚠️ 2026-09-14 (#86): 已删除 `getSummary()`(GET /analytics/summary)。
// 该接口已废弃(后端公式含卡商且不扣重叠,与 #53/#81 口径冲突),
// 且本函数**全仓零调用点**(只有定义无使用)→ 随后端接口一并清理。
// 运营分析
export const operation = {