feat: 新增总览工作台(仅管理员)

- 新增总览工作台,聚合学术/科普/科研/医患/MCN数据
- 4个聚合API: 预算/发生月报季报
- 新增预算总览/发生总览侧边栏tab
- 月报/季报视图切换
- 总览仅管理员可见
- 版本号 v1.2.0.24
This commit is contained in:
mac
2026-07-11 12:26:29 +08:00
parent 75227c51c3
commit 536b165a9b
6 changed files with 438 additions and 14 deletions

View File

@@ -14,7 +14,7 @@ from helpers import add_file_index, attach_common, monthly_finance
bp = Blueprint("api", __name__)
ALL_TENANTS = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"]
ALL_TENANTS = ["总览", "科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"]
TABLES = {
"sales": ("sales_leads", ["target_customer", "priority", "status", "tenant"]),
@@ -75,7 +75,7 @@ def auth_login():
session["display_name"] = user["display_name"]
session["role"] = user["role"]
if user["role"] == "admin":
session["tenants"] = ["科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"]
session["tenants"] = ["总览", "科普·无界", "科研·无界", "医患·无界", "MCN·无界", "学术·无界", "测试·无界"]
else:
ut = rows(conn, "SELECT tenant FROM user_tenants WHERE user_id=?", (user["id"],))
session["tenants"] = [x["tenant"] for x in ut]
@@ -128,7 +128,6 @@ def auth_me():
tenants = session.get("tenants", [])
tenants = [t if t != "总工作台" else "科普·无界" for t in tenants]
tenants = [t if t != "学会·无界" else "学术·无界" for t in tenants]
tenants = [t for t in tenants if t != "总览" and t != "总工作台"]
return jsonify({
"logged_in": True,
"user": {"id": session["user_id"], "username": session["username"], "display_name": session["display_name"], "role": session["role"]},
@@ -264,7 +263,6 @@ def bootstrap():
allowed = session.get("tenants", [])
allowed = [t if t != "总工作台" else "科普·无界" for t in allowed]
allowed = [t if t != "学会·无界" else "学术·无界" for t in allowed]
allowed = [t for t in allowed if t != "总览" and t != "总工作台"]
if tenant not in allowed:
tenant = allowed[0] if allowed else ""
conn = db()
@@ -731,3 +729,177 @@ def delete_file(file_id):
@bp.route("/api/health")
def health():
return jsonify({"ok": True, "service": "opc-manager"})
# ---------- 总览聚合 API ----------
OVERVIEW_TENANTS = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
def _aggregate_monthly_from_json(conn, table, tenants):
"""聚合多个 tenant 的表 (含 budget_data/expense_data JSON) 到 12 个月数组"""
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
rev, gross, payment, cost, paid = [0]*12, [0]*12, [0]*12, [0]*12, [0]*12
sign_total = 0
sign_cnt = 0
for tenant in tenants:
pfs = rows(conn, f"SELECT sign_amount, sign_month, budget_data, expense_data FROM {table} WHERE tenant=?", [tenant])
for pf in pfs:
sm = pf.get("sign_month") or ""
if sm.startswith(str(_year)):
sign_total += float(pf.get("sign_amount") or 0)
sign_cnt += 1
try:
bd = json.loads(pf.get("budget_data") or "[]")
except:
bd = []
try:
ed = json.loads(pf.get("expense_data") or "[]")
except:
ed = []
for b in bd:
m = (b.get("month") or "")[:7]
if m in months:
i = months.index(m)
rev[i] += float(b.get("rev") or 0)
gross[i] += float(b.get("gross") or 0)
payment[i] += float(b.get("payment") or 0)
for e in ed:
m = (e.get("month") or "")[:7]
if m in months:
i = months.index(m)
cost[i] += float(e.get("cost") or 0)
paid[i] += float(e.get("paid") or 0)
return {
"months": months,
"rev": rev, "gross": gross, "payment": payment, "cost": cost, "paid": paid,
"cashflow": [payment[i] - paid[i] for i in range(12)],
"sign_total": sign_total, "sign_cnt": sign_cnt,
}
def _aggregate_expenses(conn, table, tenants):
"""聚合多个 tenant 的平台费用到 12 个月"""
_year = date.today().year
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
amount = [0]*12
incurred = [0]*12
for tenant in tenants:
exps = rows(conn, f"SELECT expense_month, amount, incurred_amount FROM {table} WHERE tenant=? AND expense_month LIKE ?", [tenant, f"{_year}-%"])
for e in exps:
m = (e.get("expense_month") or "")[:7]
if m in months:
i = months.index(m)
amount[i] += float(e.get("amount") or 0)
incurred[i] += float(e.get("incurred_amount") or 0)
return amount, incurred
def _build_overview_result(agg, exp_amount, exp_incurred):
"""根据聚合数据构建前端使用的 overview JSON"""
months = agg["months"]
rev = agg["rev"]
gross = agg["gross"]
payment = agg["payment"]
cost = agg["cost"]
paid = agg["paid"]
cf = agg["cashflow"]
# 部门损益情况
dept_profit = [gross[i] - exp_amount[i] for i in range(12)]
dept = {
"project_gross": gross,
"dept_expense": exp_amount,
"profit": dept_profit,
"annual": [sum(gross), sum(exp_amount), sum(dept_profit)],
}
# 部门现金流
bl_cf = [cf[i] - exp_incurred[i] for i in range(12)]
dept_cf = {
"project_cf": cf,
"dept_cf_item": exp_incurred,
"bl_cf": bl_cf,
"annual": [sum(cf), sum(exp_incurred), sum(bl_cf)],
}
# 项目经营详情
project = {
"sign": agg["sign_total"],
"revenue": rev,
"gross": gross,
"cost": cost,
"paid": paid,
"payment": payment,
"cashflow": cf,
"annual": [agg["sign_total"], sum(rev), sum(gross), sum(cost), sum(paid), sum(payment), sum(cf)],
}
return {"dept": dept, "dept_cf": dept_cf, "project": project, "months": months}
def _quarterly_from_monthly(arr12):
"""将 12 个月数组转换为 4 个季度"""
q_ranges = [(0,1,2),(3,4,5),(6,7,8),(9,10,11)]
return [arr12[q[0]]+arr12[q[1]]+arr12[q[2]] for q in q_ranges]
@bp.route("/api/overview/plan-monthly")
@login_required
def overview_plan_monthly():
conn = db()
agg = _aggregate_monthly_from_json(conn, "plan_finances", OVERVIEW_TENANTS)
exp_amount, exp_incurred = _aggregate_expenses(conn, "plan_expense_records", OVERVIEW_TENANTS)
conn.close()
return jsonify(_build_overview_result(agg, exp_amount, exp_incurred))
@bp.route("/api/overview/plan-quarterly")
@login_required
def overview_plan_quarterly():
conn = db()
agg = _aggregate_monthly_from_json(conn, "plan_finances", OVERVIEW_TENANTS)
exp_amount, exp_incurred = _aggregate_expenses(conn, "plan_expense_records", OVERVIEW_TENANTS)
conn.close()
# Convert monthly to quarterly
result = _build_overview_result(agg, exp_amount, exp_incurred)
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
result["project"][key] = _quarterly_from_monthly(result["project"][key])
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
result["dept"]["profit"] = _quarterly_from_monthly(result["dept"]["profit"])
result["dept_cf"]["project_cf"] = _quarterly_from_monthly(result["dept_cf"]["project_cf"])
result["dept_cf"]["dept_cf_item"] = _quarterly_from_monthly(result["dept_cf"]["dept_cf_item"])
result["dept_cf"]["bl_cf"] = _quarterly_from_monthly(result["dept_cf"]["bl_cf"])
return jsonify(result)
@bp.route("/api/overview/finance-monthly")
@login_required
def overview_finance_monthly():
conn = db()
agg = _aggregate_monthly_from_json(conn, "project_finances", OVERVIEW_TENANTS)
exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS)
conn.close()
return jsonify(_build_overview_result(agg, exp_amount, exp_incurred))
@bp.route("/api/overview/finance-quarterly")
@login_required
def overview_finance_quarterly():
conn = db()
agg = _aggregate_monthly_from_json(conn, "project_finances", OVERVIEW_TENANTS)
exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS)
conn.close()
result = _build_overview_result(agg, exp_amount, exp_incurred)
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
result["project"][key] = _quarterly_from_monthly(result["project"][key])
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
result["dept"]["profit"] = _quarterly_from_monthly(result["dept"]["profit"])
result["dept_cf"]["project_cf"] = _quarterly_from_monthly(result["dept_cf"]["project_cf"])
result["dept_cf"]["dept_cf_item"] = _quarterly_from_monthly(result["dept_cf"]["dept_cf_item"])
result["dept_cf"]["bl_cf"] = _quarterly_from_monthly(result["dept_cf"]["bl_cf"])
return jsonify(result)