feat(data): #333 数据查看树五路原始表采纳状态只读展示
Some checks failed
gitleaks / gitleaks (push) Failing after 10s
backend-test / commit-message-lint (push) Successful in 3s
backend-test / analytics-ods-isolation (push) Failing after 2s
backend-test / backend-lint-test (push) Failing after 4m59s
backend-test / frontend-lint-test (push) Has been cancelled
Some checks failed
gitleaks / gitleaks (push) Failing after 10s
backend-test / commit-message-lint (push) Successful in 3s
backend-test / analytics-ods-isolation (push) Failing after 2s
backend-test / backend-lint-test (push) Failing after 4m59s
backend-test / frontend-lint-test (push) Has been cancelled
- refund_adoption 新增 ADOPT_REASON_CN 规则码中文释义(17码, 单一来源紧贴 规则码定义), ADOPT_REASON_ENUM/ADOPT_STATE_ENUM 经 fields API 下发前端 - data_source_registry: 经营五路 A~E 程序化补采纳四列 FIELD_OVERRIDES (adopt_state 三态枚举+可筛选 / adopt_reason 释义枚举不可筛 / adopt_rule_ver+adopted_at 仅展示); 启动 lifespan 自动注册生效 - Browser.vue 三层落地: ①设计说明补 #333 段(取舍: 不提供改采纳状态入口, 人工治理一律走 adoption_overrides 留痕); ②页面层 adopt_state 列头 ⓘ 三态口径说明(含"未判定=未跑作业"); ③行级 el-tag 三态徽标 + 规则码 中文 tooltip(支持 manual: 前缀人工覆盖展示) + 采纳状态下拉筛选(待定 治理跟进); 释义前端零硬编码, 规则升级只改后端 - 测试 test_adopt_state_display_333.py 5 用例: 注册四列齐全/三态元数据/ 释义与规则码全集一致(防两处漂移)/版本与时间不可筛 - ruff 全绿; 前端 build 通过; 全量 pytest 零失败 Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
This commit is contained in:
@@ -36,6 +36,9 @@ from app.models.shanmao_refund import ShanmaoRefund
|
||||
from app.models.ticket import ServiceTicket
|
||||
from app.models.wallet import WalletWithdrawal
|
||||
|
||||
# #333: 采纳三态/规则码枚举单一来源在采纳作业模块
|
||||
from app.services.governance.refund_adoption import ADOPT_REASON_ENUM, ADOPT_STATE_ENUM
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 数据查看树:分组口径与命名约束(工单 #100)
|
||||
#
|
||||
@@ -681,6 +684,38 @@ FIELD_OVERRIDES: dict[tuple[str, str], dict] = {
|
||||
("device_flow_usage", "fetched_at"): {"role": "time", "name_cn": "抓取时间"},
|
||||
}
|
||||
|
||||
# ── #333: 采纳治理四列展示配置(经营五路 A~E 共用, 程序化展开) ────────────────
|
||||
# 目的: 数据查看树五路原始表行级展示「这行为什么进/没进统计」——
|
||||
# adopt_state 三态徽标 + 规则码中文 tooltip + 待定行可筛选(工单 #333)。
|
||||
# 口径单一来源: ADOPT_REASON_ENUM / ADOPT_STATE_ENUM 定义在采纳作业模块
|
||||
# (app.services.governance.refund_adoption), 规则码变更必须同处升版。
|
||||
# 只读约束: 本页仅展示, 人工覆盖一律走后端 admin API adoption_overrides(铁律 15)。
|
||||
_ADOPT_SOURCE_CODES = ("refund", "wallet", "shanmao_refund", "manual_refund", "refund_record_report")
|
||||
for _ac in _ADOPT_SOURCE_CODES:
|
||||
FIELD_OVERRIDES[(_ac, "adopt_state")] = {
|
||||
"role": "category",
|
||||
"enum": list(ADOPT_STATE_ENUM),
|
||||
"is_filterable": True,
|
||||
"name_cn": "采纳状态(是否进统计)",
|
||||
}
|
||||
FIELD_OVERRIDES[(_ac, "adopt_reason")] = {
|
||||
"role": "dimension",
|
||||
# enum 下发 "规则码=中文释义" 列表, 供前端 tooltip; 不参与筛选
|
||||
"enum": list(ADOPT_REASON_ENUM),
|
||||
"is_filterable": False,
|
||||
"name_cn": "采纳规则码",
|
||||
}
|
||||
FIELD_OVERRIDES[(_ac, "adopt_rule_ver")] = {
|
||||
"role": "dimension",
|
||||
"is_filterable": False,
|
||||
"name_cn": "采纳规则版本",
|
||||
}
|
||||
FIELD_OVERRIDES[(_ac, "adopted_at")] = {
|
||||
"role": "time",
|
||||
"is_filterable": False,
|
||||
"name_cn": "采纳判定时间",
|
||||
}
|
||||
|
||||
# 反射时排除的系统字段(TimestampMixin 的系统时间 + raw_extra 冗余)
|
||||
_EXCLUDED_FIELDS = {"id", "created_at", "updated_at", "raw_extra"}
|
||||
|
||||
|
||||
@@ -68,6 +68,36 @@ E_ADOPT_ALL = "E_ADOPT_ALL" # 全量采纳(refund_status 原样
|
||||
# 未知枚举(不在任何白名单内)→ pending; 非业务规则码, 仅标记「机器无法归类」
|
||||
PENDING_UNKNOWN = "PENDING_UNKNOWN"
|
||||
|
||||
# ── 规则码中文释义(#333) ─────────────────────────────────────────────────────
|
||||
# 单一来源: 与上方规则码定义同处维护(改规则码必须同步改这里)。
|
||||
# 消费方: data_source_registry 经 ADOPT_REASON_ENUM 下发前端, 数据查看树
|
||||
# adopt_reason 列 tooltip 展示; 供业务看懂「这行为什么进/没进统计」。
|
||||
ADOPT_REASON_CN: dict[str, str] = {
|
||||
A_ADOPT_ONLINE: "采纳: 已退款且方式为线上(含空方式)",
|
||||
A_EX_METHOD: "排除: 方式为转入提现账户/线下, 非线上直退",
|
||||
A_EX_GARBAGE: "排除: 方式或状态错位值/长文本垃圾",
|
||||
A_EX_STATUS: "排除: 状态为无需退款/驳回/申请中(未完结)",
|
||||
A_EX_STATUS_NULL: "排除: 状态为空(且非已退款)",
|
||||
B_ADOPT_INFLOW: "采纳: 订单退费转入(蓄水池进水)",
|
||||
B_ADOPT_OUTFLOW: "采纳: 系统扣费/成功用户提现(出水)",
|
||||
B_EX_FAIL_FROZEN: "排除: 用户提现备注含失败/冻结关键词",
|
||||
B_EX_NULL_TYPE: "排除: 操作类型为空",
|
||||
C_ADOPT: "采纳: 闪猫退款状态为已退款",
|
||||
C_EX_NOT_REFUNDED: "排除: 审核中/退款中/失败/驳回/无需退款",
|
||||
D_ADOPT: "采纳: 手动登记已退款(非重复/SN可解析)",
|
||||
D_EX_DUP: "排除: 重复残留(dup_residual)",
|
||||
D_EX_NO_SN: "排除: SN不可解析(sn_unresolvable)",
|
||||
D_EX_NOT_REFUNDED: "排除: 登记为未退款(蓄水, 非出款)",
|
||||
E_ADOPT_ALL: "采纳: E路全量(水单对账基准, 状态原样保留)",
|
||||
PENDING_UNKNOWN: "待定: 规则白名单外的未知枚举, 待业务确认",
|
||||
}
|
||||
|
||||
# 前端下发格式: "code=中文" 列表(经 DataSourceField.enum_values 走既有通道)
|
||||
ADOPT_REASON_ENUM: list[str] = [f"{k}={v}" for k, v in ADOPT_REASON_CN.items()]
|
||||
|
||||
# adopt_state 三态枚举(含 NULL 语义说明见 AdoptionColumnsMixin 注释)
|
||||
ADOPT_STATE_ENUM: list[str] = ["adopted", "excluded", "pending"]
|
||||
|
||||
ROUTE_A = "A"
|
||||
ROUTE_B = "B"
|
||||
ROUTE_C = "C"
|
||||
|
||||
87
backend/tests/test_adopt_state_display_333.py
Normal file
87
backend/tests/test_adopt_state_display_333.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""采纳治理四列在数据查看树的展示配置(#333)
|
||||
|
||||
锁定:
|
||||
1. 经营五路 A~E 注册条目含 adopt_state/adopt_reason/adopt_rule_ver/adopted_at
|
||||
四列, 且中文名/枚举/可筛选属性正确(经 ensure_data_sources_registered 反射落库);
|
||||
2. adopt_reason enum 下发 "规则码=中文释义" 格式, 与采纳作业模块规则码全集一致
|
||||
(改规则码不改释义 → 本测试失败, 防两处漂移);
|
||||
3. 采纳作业模块 ADOPT_REASON_CN 覆盖 ADOPT_STATE 相关全部规则码常量。
|
||||
|
||||
为什么测注册结果而不只测 FIELD_OVERRIDES 常量: #100 教训是「配置对不代表渲染对」
|
||||
—— 前端完全依赖 /data-sources/{code}/fields 下发的字段元数据, 注册漏了列, 界面
|
||||
就没有该列; enum 断在注册层, tooltip 就没有释义。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.data_source import DataSource, DataSourceField
|
||||
from app.services.data_source_registry import ensure_data_sources_registered
|
||||
from app.services.governance.refund_adoption import (
|
||||
ADOPT_REASON_CN,
|
||||
ADOPT_STATE_ENUM,
|
||||
)
|
||||
|
||||
_ADOPT_CODES = ("refund", "wallet", "shanmao_refund", "manual_refund", "refund_record_report")
|
||||
_ADOPT_FIELDS = ("adopt_state", "adopt_reason", "adopt_rule_ver", "adopted_at")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def registered(db: Session):
|
||||
"""跑一次幂等注册, 返回 code → {field_name: DataSourceField} 映射。"""
|
||||
ensure_data_sources_registered(db)
|
||||
db.commit()
|
||||
out: dict[str, dict[str, DataSourceField]] = {}
|
||||
for code in _ADOPT_CODES:
|
||||
ds = db.scalar(select(DataSource).where(DataSource.code == code))
|
||||
fields = list(db.scalars(select(DataSourceField).where(DataSourceField.source_id == ds.id)))
|
||||
out[code] = {f.field_name: f for f in fields}
|
||||
return out
|
||||
|
||||
|
||||
class TestAdoptFieldRegistration:
|
||||
def test_all_five_sources_have_adopt_fields(self, registered):
|
||||
"""★ 五路原始表都必须注册采纳四列(漏注册 = 界面没有该列)"""
|
||||
for code, fmap in registered.items():
|
||||
missing = [f for f in _ADOPT_FIELDS if f not in fmap]
|
||||
assert not missing, f"{code} 缺采纳字段: {missing}"
|
||||
|
||||
def test_adopt_state_meta(self, registered):
|
||||
"""adopt_state: 三态枚举 + 可筛选(待定行可过滤的依据) + 中文名"""
|
||||
for code, fmap in registered.items():
|
||||
f = fmap["adopt_state"]
|
||||
assert f.enum_values == ADOPT_STATE_ENUM
|
||||
assert f.is_filterable is True
|
||||
assert "采纳状态" in (f.field_name_cn or "")
|
||||
|
||||
def test_adopt_reason_meta(self, registered):
|
||||
"""adopt_reason: enum 为 '规则码=中文释义' 格式且覆盖规则码全集; 不可筛选"""
|
||||
for code, fmap in registered.items():
|
||||
f = fmap["adopt_reason"]
|
||||
assert f.is_filterable is False
|
||||
enums = f.enum_values or []
|
||||
parsed = dict(e.split("=", 1) for e in enums)
|
||||
assert set(parsed) == set(ADOPT_REASON_CN), f"{code} 规则码释义与全集不一致"
|
||||
|
||||
def test_adopt_reason_covers_all_rule_constants(self):
|
||||
"""★ 规则码常量全集必须都有中文释义(新增规则码忘加释义 → 在此拦截)"""
|
||||
import re
|
||||
|
||||
import app.services.governance.refund_adoption as m
|
||||
|
||||
consts = {
|
||||
v for k, v in vars(m).items()
|
||||
if re.fullmatch(r"[ABCDE]_(ADOPT|EX)_[A-Z_]+", k or "")
|
||||
}
|
||||
consts.add(m.PENDING_UNKNOWN)
|
||||
assert consts <= set(ADOPT_REASON_CN), f"缺释义的规则码: {consts - set(ADOPT_REASON_CN)}"
|
||||
|
||||
def test_adopt_ver_and_time_not_filterable(self, registered):
|
||||
"""adopt_rule_ver/adopted_at 仅展示, 不进筛选(避免误把版本当业务条件)"""
|
||||
for code, fmap in registered.items():
|
||||
assert fmap["adopt_rule_ver"].is_filterable is False
|
||||
assert fmap["adopted_at"].is_filterable is False
|
||||
@@ -29,6 +29,14 @@
|
||||
因为投诉口径是 `service_category` 空白或='投诉'(BMS 导出未选类别即空白),
|
||||
预置筛很容易与「投诉分析」页的口径打架。要看投诉口径请去投诉分析页。
|
||||
· **不做树内搜索/拖拽排序**。当前 21 项,五分组可一屏扫完;加搜索是过度设计。
|
||||
· **#333 采纳状态只读展示**。经营五路原始表行级展示采纳三态徽标 + 规则码
|
||||
中文 tooltip,回答"这行原始数据到底进没进统计"(口径权威:
|
||||
docs/standards/退款五路数据标准与采纳规则.md)。规则码释义由后端
|
||||
/fields 的 enum_values 下发(单一来源在采纳作业模块 refund_adoption),
|
||||
前端零硬编码,规则升级只改后端。
|
||||
取舍:**不提供改采纳状态的任何入口** —— 人工治理一律走后端 admin API
|
||||
(adoption_overrides)留痕 + 幂等重算(铁律 15),界面直改会绕过覆盖
|
||||
审计与规则版本化,故本页四列全部只读。
|
||||
-->
|
||||
<div class="flex-row">
|
||||
<!-- 左侧数据源列表 -->
|
||||
@@ -181,6 +189,20 @@
|
||||
>只读投影 · 真身「{{ currentSource.true_body_name }}」</el-tag>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<!-- #333: 采纳状态筛选(仅经营五路原始表显示) —— 待定行治理跟进入口 -->
|
||||
<el-select
|
||||
v-if="hasAdoptState"
|
||||
v-model="filters.adopt_state"
|
||||
size="small"
|
||||
style="width: 160px"
|
||||
placeholder="采纳状态: 全部"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<el-option label="采纳(进统计)" value="adopted" />
|
||||
<el-option label="排除(不进统计)" value="excluded" />
|
||||
<el-option label="待定(需治理跟进)" value="pending" />
|
||||
</el-select>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -216,7 +238,50 @@
|
||||
:sortable="col.sortable"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ fmtCell(row[col.prop], col.dataType) }}</template>
|
||||
<!-- #333: 采纳状态列表头带口径说明(ⓘ) —— 页面层可见说明 -->
|
||||
<template v-if="col.prop === 'adopt_state'" #header>
|
||||
<span>{{ col.label }}</span>
|
||||
<el-tooltip placement="top" effect="light" :width="340">
|
||||
<template #content>
|
||||
<div style="line-height: 1.7">
|
||||
<b>这行原始数据进没进统计?</b>(口径:退款五路数据标准与采纳规则.md)<br>
|
||||
<el-tag size="small" type="success">采纳</el-tag> 进入 std_* 标准表,计入经营统计<br>
|
||||
<el-tag size="small" type="info">排除</el-tag> 规则排除,不计入统计(原因见「采纳规则码」列)<br>
|
||||
<el-tag size="small" type="warning">待定</el-tag> 未知枚举,待业务确认,暂不计入<br>
|
||||
<span style="opacity:.7">未判定 = 尚未跑过采纳作业(新同步行)。<br>
|
||||
本页只读:改判走后台人工覆盖 API 留痕,规则版本化可重算。</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-icon class="adopt-info-icon"><InfoFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<!-- #333: 采纳三态徽标 -->
|
||||
<template v-if="col.prop === 'adopt_state'">
|
||||
<el-tag
|
||||
v-if="row.adopt_state"
|
||||
size="small"
|
||||
:type="adoptStateMeta[row.adopt_state]?.type || 'info'"
|
||||
>{{ adoptStateMeta[row.adopt_state]?.label || row.adopt_state }}</el-tag>
|
||||
<span v-else class="cell-dim">未判定</span>
|
||||
</template>
|
||||
<!-- #333: 规则码 + 中文释义 tooltip(释义来自后端 enum 下发) -->
|
||||
<template v-else-if="col.prop === 'adopt_reason'">
|
||||
<el-tooltip v-if="row.adopt_reason" placement="top" effect="light" :width="320">
|
||||
<template #content>
|
||||
<div style="max-width: 300px; line-height: 1.6">
|
||||
{{ adoptReasonCn(row.adopt_reason) }}
|
||||
<div v-if="row.adopt_rule_ver" style="margin-top: 4px; opacity: .7">
|
||||
规则版本: {{ row.adopt_rule_ver }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<span class="adopt-reason-cell">{{ row.adopt_reason }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else>{{ fmtCell(row[col.prop], col.dataType) }}</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
@@ -434,6 +499,10 @@ const tableColumns = computed(() => {
|
||||
if (f.data_type === 'timestamp') width = 170
|
||||
if (f.data_type === 'numeric') width = 120
|
||||
if (f.semantic_role === 'sn') width = 180
|
||||
// #333: 采纳治理四列固定列宽(徽标/规则码/版本/时间)
|
||||
if (f.field_name === 'adopt_state') width = 150
|
||||
if (f.field_name === 'adopt_reason') width = 150
|
||||
if (f.field_name === 'adopt_rule_ver') width = 130
|
||||
return {
|
||||
prop: f.field_name,
|
||||
label: f.field_name_cn || f.field_name,
|
||||
@@ -444,6 +513,40 @@ const tableColumns = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
// ── #333: 采纳治理四列展示辅助 ────────────────────────────────────────────
|
||||
// 三态徽标文案与颜色(纯展示映射; 枚举值本身由后端权威定义)
|
||||
const adoptStateMeta = {
|
||||
adopted: { label: '采纳', type: 'success' },
|
||||
excluded: { label: '排除', type: 'info' },
|
||||
pending: { label: '待定', type: 'warning' },
|
||||
}
|
||||
|
||||
// 当前数据源是否含采纳状态列(经营五路原始表; 其它表不显示筛选与徽标逻辑)
|
||||
const hasAdoptState = computed(() =>
|
||||
currentFields.value.some((f) => f.field_name === 'adopt_state')
|
||||
)
|
||||
|
||||
// 规则码 → 中文释义: 从后端 /fields 的 adopt_reason enum_values("code=中文")解析,
|
||||
// 前端零硬编码 —— 规则码升级只改采纳作业模块, 界面自动跟随。
|
||||
const adoptReasonMap = computed(() => {
|
||||
const f = currentFields.value.find((x) => x.field_name === 'adopt_reason')
|
||||
const map = {}
|
||||
for (const e of f?.enum_values || []) {
|
||||
const i = e.indexOf('=')
|
||||
if (i > 0) map[e.slice(0, i)] = e.slice(i + 1)
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
function adoptReasonCn(code) {
|
||||
// 人工覆盖行带 manual: 前缀(机器规则码仍保留, 见采纳作业模块人工覆盖约定)
|
||||
if (code?.startsWith('manual:')) {
|
||||
const raw = code.slice(7)
|
||||
return `人工覆盖(原规则: ${adoptReasonMap.value[raw] || raw})`
|
||||
}
|
||||
return adoptReasonMap.value[code] || code
|
||||
}
|
||||
|
||||
function fmtCell(value, dataType) {
|
||||
if (value == null) return '-'
|
||||
if (dataType === 'numeric') {
|
||||
@@ -714,4 +817,18 @@ onMounted(() => {
|
||||
color: #909399;
|
||||
}
|
||||
.placeholder-tag { margin-left: 6px; }
|
||||
|
||||
/* #333: 采纳治理列展示样式 */
|
||||
.adopt-info-icon {
|
||||
margin-left: 2px;
|
||||
vertical-align: -2px;
|
||||
color: #909399;
|
||||
cursor: help;
|
||||
}
|
||||
.adopt-reason-cell {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
cursor: help;
|
||||
}
|
||||
.cell-dim { color: #c0c4cc; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user