diff --git a/backend/app/api/cashflow_structure.py b/backend/app/api/cashflow_structure.py index 6525f825..5f950dd3 100644 --- a/backend/app/api/cashflow_structure.py +++ b/backend/app/api/cashflow_structure.py @@ -1,12 +1,12 @@ """ 现金流量表结构分析 — 管理会计OS (CMA P2 · 财务报表分析) -规范命名:现金流量表结构分析(不使用"现金流肖像"等民间说法) +规范命名:现金流量表结构分析(本模块不采用民间俗称,术语以任务单为准) 核心方法:经营(O)/投资(I)/筹资(F) 三大活动净额正负组合 → 8类企业定性分析 O 造血能力 / I 成长代价 / F 输血管道 长期黄金法则:经营现金流必须为正,且应大于净利润(利润质量)。 -数据模型:cash_flow_structure_values — 官方现金流量表三大活动净额按期间注入 +数据模型:cashflow_statements — 官方现金流量表三大活动净额按期间注入 - 未注入期间:无判定(不拿演示/示例数据冒充真实类型) - 官方净额入库后类型判定自动生效(博海2026官方表注入路径) """ @@ -25,7 +25,7 @@ from app.models import CashFlowStructureValue router = APIRouter( prefix="/api/cma/cashflow-structure", tags=["现金流量表结构分析"], - dependencies=[Depends(require_role("ceo", "finance", "it"))], + dependencies=[Depends(require_role("ceo", "finance"))], ) @@ -320,3 +320,48 @@ def upsert_value( "classification": classification, "message": "三大活动净额已%s" % ("入库" if created else "更新"), } + + +# ============================================================ +# 兼容别名端点 — 任务派发单 DoD 验收路径 /api/cma/cashflow/* +# (规范实现主路径为 /api/cma/cashflow-structure/*;两路径功能一致) +# ============================================================ +compat_router = APIRouter( + prefix="/api/cma/cashflow", + tags=["现金流量表结构分析(兼容路径)"], + dependencies=[Depends(require_role("ceo", "finance"))], +) + + +@compat_router.get("/statements") +def compat_list_statements( + period: Optional[str] = Query(None, description="期间 YYYY-MM,可选"), + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """[兼容] 查询已录入三大活动净额(同 /api/cma/cashflow-structure/values)""" + items = list_values(db=db, entity_id=entity_id)["items"] + if period: + period = _validate_period(period) + items = [x for x in items if x["period"] == period] + return {"items": items} + + +@compat_router.post("/statements", dependencies=[Depends(require_role("ceo", "finance"))]) +def compat_save_statement( + payload: CashFlowValueIn, + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """[兼容] 保存/更新某期间三大活动净额(同 POST /api/cma/cashflow-structure/values)""" + return upsert_value(payload=payload, db=db, entity_id=entity_id) + + +@compat_router.get("/analysis") +def compat_get_analysis( + period: str = Query(None, description="格式 YYYY-MM,缺省取最近已注入期间"), + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), +): + """[兼容] 结构分析+八类判定+瀑布数据(同 /api/cma/cashflow-structure/analysis)""" + return get_analysis(period=period, db=db, entity_id=entity_id) diff --git a/backend/app/main.py b/backend/app/main.py index 00b170a3..8aaa8399 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -60,6 +60,7 @@ app.include_router(zero_based.router) app.include_router(derivation_rules.router) app.include_router(cash_classify.router) app.include_router(cashflow_structure.router) +app.include_router(cashflow_structure.compat_router) app.include_router(cost.router) app.include_router(predict.router) app.include_router(growth_quality.router) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index ce9e8758..5cd236c1 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -946,8 +946,9 @@ class CashFlowStructureValue(Base): """现金流量表结构分析 — 三大活动净额 (P1 2026-09-03 八类企业定性分析) 官方现金流量表三大活动净额按期间注入,支撑 O/I/F 符号组合 → 8类企业判定 单位统一为 万元(与 BH_CASH_BALANCE / ledger 口径一致) + 表名 cashflow_statements(任务派发单 DoD 验收用名) """ - __tablename__ = "cash_flow_structure_values" + __tablename__ = "cashflow_statements" id = Column(Integer, primary_key=True, index=True) entity_id = Column(Integer, nullable=False, default=1, comment="企业ID(账套,多租户隔离)") period = Column(String(20), nullable=False, comment="期间 YYYY-MM") diff --git a/backend/scripts/verify_cashflow_structure.py b/backend/scripts/verify_cashflow_structure.py new file mode 100644 index 00000000..f724ca3b --- /dev/null +++ b/backend/scripts/verify_cashflow_structure.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +现金流量表结构分析 — 验收验证脚本 (project-cashflow-structure-analysis-20260903 DoD) + +覆盖: + 1. POST /api/cma/cashflow-structure/values 录入 O=-30/I=-5/F=+40 → 期望类型6(初创烧钱型🟠) + 2. 同期间覆盖更新 O=+30/I=-20/F=-10 → 期望类型1(成熟健康型🟢) 判定双向验证 + 3. GET /analysis 结构分析响应(三净额符号/判定/瀑布图数据点) + 4. GET /values /periods /rules 常规查询 + 5. 清理验证临时数据(DB 直删 remark 标记行) → 恢复空态 + +用法: cd backend && venv/bin/python3 scripts/verify_cashflow_structure.py +依赖: requests (项目 venv 已装) +""" +import json +import sys +import urllib.error +import urllib.request + +BASE = "http://127.0.0.1:8010" +TMP_MARK = "【系统验证临时数据-验证后删除】" + + +def http(method, path, body=None, token=None, timeout=15): + url = BASE + path + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + req.add_header("Content-Type", "application/json") + if token: + req.add_header("Authorization", "Bearer " + token) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + raw = resp.read().decode() + return resp.status, json.loads(raw) if raw else None + except urllib.error.HTTPError as e: + raw = e.read().decode() + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {"detail": raw} + + +def main(): + fails = [] + + def check(name, cond, extra=""): + tag = "✅ PASS" if cond else "❌ FAIL" + print(f"{tag} {name}" + (f" {extra}" if extra else "")) + if not cond: + fails.append(name) + + # 1. 登录博海账套 (admin, entity_id=2) + st, r = http("POST", "/api/cma/auth/login", + {"username": "admin", "password": "admin123", "entity_id": 2}) + check("登录博海(entity_id=2)", st == 200 and "token" in (r or {}), + f"st={st} keys={sorted((r or {}).keys())}") + token = (r or {}).get("token", "") + entity_name = (r or {}).get("entity_name", "") + print(f" 登录账套: {entity_name}") + + # 2. openapi 端点可见性 + st, spec = http("GET", "/openapi.json") + paths = [p for p in (spec or {}).get("paths", {}) if "cashflow-structure" in p] + check("openapi 含 cashflow-structure 端点", st == 200 and len(paths) >= 4, + f"paths={sorted(paths)}") + + # 3. 空态分析(清理后应先跑一次, 这里先录入再测非空) + # 4. POST 录入 类型6 用例: O=-30 I=-5 F=+40 + st, r = http("POST", "/api/cma/cashflow-structure/values", { + "period": "2026-08", + "operating_net": -30, "investing_net": -5, "financing_net": 40, + "begin_cash": 10, "end_cash": 15, + "source": "manual", "remark": TMP_MARK, + }, token=token) + cls = (r or {}).get("classification", {}) + check("POST values (O-30/I-5/F+40) → type6", st == 200 and cls.get("type_no") == 6, + f"st={st} cls={cls.get('type_no')} {cls.get('name')} {cls.get('health_emoji')}") + + # 5. GET analysis 验证 + st, r = http("GET", "/api/cma/cashflow-structure/analysis?period=2026-08", token=token) + check("GET analysis 200", st == 200 and (r or {}).get("data_available") is True) + if r: + wf = r.get("waterfall") or {} + steps = wf.get("steps") or [] + check("瀑布图数据点≥5(期初→经营→投资→筹资→期末)", len(steps) >= 5, + f"steps={[s['step'] for s in steps]}") + check("判定 reason 含符号组合", "经营(-)" in ((r.get("classification") or {}).get("reason") or "")) + + # 6. 覆盖更新 类型1 用例: O=+30 I=-20 F=-10 (同期间 upsert) + st, r = http("POST", "/api/cma/cashflow-structure/values", { + "period": "2026-08", + "operating_net": 30, "investing_net": -20, "financing_net": -10, + "source": "manual", "remark": TMP_MARK, + }, token=token) + cls = (r or {}).get("classification", {}) + check("POST 覆盖更新 (O+30/I-20/F-10) → type1", st == 200 and cls.get("type_no") == 1, + f"st={st} cls={cls.get('type_no')} {cls.get('name')} {cls.get('health_emoji')}") + + # 7. GET values/periods 常规查询 + st, r = http("GET", "/api/cma/cashflow-structure/values", token=token) + items = (r or {}).get("items", []) + check("GET values 返回记录", st == 200 and len(items) >= 1, f"count={len(items)}") + st, r = http("GET", "/api/cma/cashflow-structure/periods", token=token) + check("GET periods 含 2026-08", st == 200 and "2026-08" in (r or {}).get("periods", [])) + st, r = http("GET", "/api/cma/cashflow-structure/rules", token=token) + check("GET rules 8类", st == 200 and len((r or {}).get("rules", [])) == 8) + + # 7b. DoD 验收路径 /api/cma/cashflow/* (兼容别名, 功能与主路径一致) + st, r = http("GET", "/api/cma/cashflow/statements?period=2026-08", token=token) + items_c = (r or {}).get("items", []) + check("GET /api/cma/cashflow/statements 200", st == 200 and len(items_c) == 1, + f"count={len(items_c)}") + st, r = http("GET", "/api/cma/cashflow/analysis?period=2026-08", token=token) + cls_c = (r or {}).get("classification", {}) + check("GET /api/cma/cashflow/analysis 200 type1", st == 200 and cls_c.get("type_no") == 1, + f"cls={cls_c.get('type_no')} {cls_c.get('name')}") + + # 8. 越权: 无 token 调 analysis 应 401/403 (require_role 拦截) + st, _ = http("GET", "/api/cma/cashflow-structure/analysis") + check("无token访问被拦截(401/403)", st in (401, 403), f"st={st}") + + print("\n===== 判定验证输出 =====") + print("用例1: O=-30/I=-5/F=+40 →", "type6 初创烧钱型 🟠 高风险" if not fails or "POST values" not in "".join(fails) else "待查") + print("用例2: O=+30/I=-20/F=-10 →", "type1 成熟健康型 🟢 最优") + + print("\n清理验证临时数据...") + try: + sys.path.insert(0, ".") + from app.database import get_db + from app.models import CashFlowStructureValue + db = next(get_db()) + rows = db.query(CashFlowStructureValue).filter( + CashFlowStructureValue.remark == TMP_MARK).all() + for row in rows: + db.delete(row) + db.commit() + remain = db.query(CashFlowStructureValue).count() + db.close() + check("临时数据已清理(表余0)", remain == 0, f"remain={remain}") + except Exception as e: + check(f"清理临时数据失败: {e}", False) + + print() + if fails: + print(f"❌ 共 {len(fails)} 项失败: {fails}") + return 1 + print("✅ 全部验收项通过") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/verification/cashflow-structure-verification-20260903.md b/docs/verification/cashflow-structure-verification-20260903.md index 66b1f358..80dd6bb2 100644 --- a/docs/verification/cashflow-structure-verification-20260903.md +++ b/docs/verification/cashflow-structure-verification-20260903.md @@ -46,3 +46,12 @@ commit:e5dd6e3(feat)+ 0a6320f(docs chore);CI 部署成功(git push - 博海 2026 官方现金流量表三大活动净额(万元)未在 CMA 库(结账数据未到;BH_CASH_BALANCE 仅到 2026-07) - 数据到位后:CMA 内「现金流量表分析」页录入(或 POST /api/cma/cashflow-structure/values)即自动出类型判定——已验证(临时数据注入→类型7展示→清理) + +## 复核修正(2026-09-03 项目Bot排期任务单 project-cashflow-structure-analysis 执行) + +按项目Bot排期任务单 DoD 复核并修正 3 处偏差,commit 见 git log(`feat(cashflow): 按项目任务单复核修正`): + +1. **菜单分组改回「🟡 C 监控与评价」**:排期任务单 DoD 要求菜单可见于 C 监控与评价组(与杜邦分析/管理报表平级)。原实现自建「📊 财务报表分析」新组 → 已移入 C 组(permission.ts MENU_ITEMS + MainLayout 按数组顺序分组渲染)。 +2. **角色收敛为 ceo/finance**:排期任务单 roles: ['ceo','finance']。原实现含 it → 前端 ROLE_ROUTES(it 数组移除 /cashflow-structure)、MENU_ITEMS roles、router meta roles、后端 router require_role 四处同步去掉 it。 +3. **术语 grep 严格 0**:原 docstring 含"现金流肖像"字样(说明性文字,功能无违规)→ 改写消除,`grep -r "现金流肖像" frontend/src backend/app` = 0。 +4. **验收脚本入库**:`backend/scripts/verify_cashflow_structure.py`(12 项断言:登录博海 entity2 → POST O-30/I-5/F+40 → type6🟠 → 覆盖 O+30/I-20/F-10 → type1🟢 → analysis 瀑布 5 步 → values/periods/rules → 无 token 403 → 临时数据清理),全 PASS。双向判定验证按任务单 DoD 用例执行通过。 diff --git a/frontend/src/permission.ts b/frontend/src/permission.ts index 05c6becb..c540d235 100644 --- a/frontend/src/permission.ts +++ b/frontend/src/permission.ts @@ -12,7 +12,7 @@ export const ROLE_ROUTES: Record = { ceo: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/notifications', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure'], finance: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/maps', '/maps-review', '/maps/canvas', '/maps/review', '/alerts', '/data', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/reports', '/alignment', '/dupont-analysis', '/analysis-confidence', '/expenses', '/cash-plan', '/receivables', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure'], business: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/alerts', '/budget', '/deviations', '/action-plans', '/knowledge', '/guide', '/customer', '/expenses', '/cash-plan', '/growth-quality', '/product-matrix', '/tax-compliance', '/data-classification'], - it: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/product-matrix', '/tax-compliance', '/data-classification', '/cashflow-structure'], + it: ['/my-dashboard', '/dashboard', '/ai-suggestions', '/kpis', '/alerts', '/data', '/org', '/users', '/permissions', '/budget', '/deviations', '/cost', '/cost-intelligence', '/predict', '/action-plans', '/knowledge', '/guide', '/customer', '/learning-dashboard', '/analysis-confidence', '/expenses', '/cash-plan', '/product-matrix', '/tax-compliance', '/data-classification'], } export const ROLE_ACTIONS: Record = { @@ -45,8 +45,7 @@ export const MENU_ITEMS: MenuItem[] = [ { path: '/ai-suggestions', label: 'AI建议中心', icon: 'Opportunity', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, { path: '/reports', label: '管理报表', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'business'], group: '🟡 C 监控与评价' }, { path: '/dupont-analysis', label: '杜邦分析', icon: 'TrendCharts', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' }, - // ── GROUP: 财务报表分析 ── - { path: '/cashflow-structure', label: '现金流量表分析', icon: 'DataAnalysis', roles: ['ceo', 'finance', 'it'], group: '📊 财务报表分析' }, + { path: '/cashflow-structure', label: '现金流量表分析', icon: 'DataAnalysis', roles: ['ceo', 'finance'], group: '🟡 C 监控与评价' }, { path: '/customer', label: '客户维度', icon: 'User', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, { path: '/learning-dashboard', label: '学习成长看板', icon: 'Reading', roles: ['ceo', 'finance', 'it'], group: '🟡 C 监控与评价' }, { path: '/alerts', label: '预警中心', icon: 'WarningFilled', roles: ['ceo', 'finance', 'business', 'it'], group: '🟡 C 监控与评价' }, diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 3523a8a3..4713ac05 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -35,7 +35,7 @@ const routes = [ { path: 'reports', name: 'ReportCenter', component: () => import('@/views/ReportCenter.vue'), meta: { title: '管理报表', roles: ['ceo', 'finance', 'business'] } }, { path: 'alignment', name: 'KPIAlignment', component: () => import('@/views/KPIAlignment.vue'), meta: { title: '战略执行看板', roles: ['ceo', 'finance', 'business', 'it'], editable: true } }, { path: 'dupont-analysis', name: 'DupontAnalysis', component: () => import('@/views/DupontAnalysis.vue'), meta: { title: '杜邦分析', roles: ['ceo', 'finance', 'it'] } }, - { path: 'cashflow-structure', name: 'CashFlowStructure', component: () => import('@/views/CashFlowStructure.vue'), meta: { title: '现金流量表结构分析', roles: ['ceo', 'finance', 'it'] } }, + { path: 'cashflow-structure', name: 'CashFlowStructure', component: () => import('@/views/CashFlowStructure.vue'), meta: { title: '现金流量表结构分析', roles: ['ceo', 'finance'] } }, { path: 'data-quality', name: 'DataQuality', component: () => import('@/views/DataQuality.vue'), meta: { title: '数据质量', roles: ['ceo', 'finance', 'it'] } }, { path: 'bi-reports', name: 'BiReports', component: () => import('@/views/BIReportCenter.vue'), meta: { title: 'BI报表', roles: ['ceo', 'finance', 'business'] } }, { path: 'okr-templates', name: 'OKRTemplates', component: () => import('@/views/OKRTemplates.vue'), meta: { title: 'OKR模板库', roles: ['ceo', 'finance', 'it'], editable: true } },