From 25cfa19a480e9d62185ab19c911ec23d73bbc479 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Fri, 24 Jul 2026 13:08:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=86=E9=94=80=E5=95=86=E8=B4=A2?= =?UTF-8?q?=E5=8A=A1=E4=BB=AA=E8=A1=A8=E7=9B=98(=E6=94=B6=E5=85=A5/?= =?UTF-8?q?=E6=88=90=E6=9C=AC/=E6=AF=9B=E5=88=A9=C3=97=E5=B8=81=E7=A7=8D,?= =?UTF-8?q?=E4=BB=8A=E5=A4=A9/=E6=9C=AC=E6=9C=88/=E6=9C=AC=E5=B9=B4/?= =?UTF-8?q?=E7=B4=AF=E8=AE=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sage_datamart/dashboards.py | 73 ++++++++++++++++++++++++++ sage_datamart/init.py | 37 +++++++++++++ scripts/load_path.py | 2 + wwwroot/api/distributor_currency.dspy | 6 +++ wwwroot/api/distributor_financial.dspy | 13 +++++ wwwroot/index.ui | 12 +++++ wwwroot/sect_distributor_currency.ui | 9 ++++ wwwroot/sect_distributor_financial.ui | 56 ++++++++++++++++++++ 8 files changed, 208 insertions(+) create mode 100644 wwwroot/api/distributor_currency.dspy create mode 100644 wwwroot/api/distributor_financial.dspy create mode 100644 wwwroot/sect_distributor_currency.ui create mode 100644 wwwroot/sect_distributor_financial.ui diff --git a/sage_datamart/dashboards.py b/sage_datamart/dashboards.py index a56f1bd..a0278a0 100644 --- a/sage_datamart/dashboards.py +++ b/sage_datamart/dashboards.py @@ -418,3 +418,76 @@ async def get_scoped_fail_count(sor, today, scope=None, org_filter=None): ns['org'] = org_filter recs = await sor.sqlExe(sql, ns) return int(recs[0].cnt) if recs else 0 + + +# ── 分销商财务统计 ── + +from datetime import date as _date_module_date + +async def get_distributor_financial(sor, org_filter, period='today'): + """分销商财务概览:按币种统计收入/成本/毛利 + period: 'today' | 'month' | 'year' | 'all' + 收入含直接客户 + 二级分销商 + """ + today_str = _date_module_date.today().isoformat() + + date_sql = "" + ns = {'org': org_filter, 'today': today_str} + if period == 'month': + date_sql = "AND f.call_date BETWEEN ${month_start}$ AND ${today}$" + ns['month_start'] = today_str[:7] + '-01' + elif period == 'year': + date_sql = "AND f.call_date BETWEEN ${year_start}$ AND ${today}$" + ns['year_start'] = today_str[:4] + '-01-01' + elif period == 'today': + date_sql = "AND f.call_date = ${today}$" + + # 收入按币种(含直接客户 + 二级分销商) + sql_rev = f"""SELECT + COALESCE(f.currency, 'CNY') as currency, + COUNT(*) as call_count, + COALESCE(SUM(f.amount), 0) as revenue, + COALESCE(SUM(f.amount_cny), 0) as revenue_cny + FROM dm_model_call_fact f + WHERE f.distributor_orgid = ${{org}}$ {date_sql} + GROUP BY f.currency ORDER BY revenue_cny DESC""" + + recs = await sor.sqlExe(sql_rev, ns) + + revenue_by_currency = [] + total_revenue_cny = 0.0 + total_calls = 0 + for r in recs: + total_revenue_cny += float(r.revenue_cny or 0) + total_calls += int(r.call_count or 0) + revenue_by_currency.append({ + 'currency': r.currency, + 'call_count': int(r.call_count or 0), + 'revenue': round(float(r.revenue or 0), 2), + 'revenue_cny': round(float(r.revenue_cny or 0), 2), + }) + + # 成本(尝试从provider_cost归集,关联distributor_orgid) + total_cost_cny = 0.0 + try: + sql_cost = f"""SELECT COALESCE(SUM(c.total_amount_cny), 0) as total_cost_cny + FROM dm_provider_cost_daily c + JOIN dm_model_call_fact f ON f.call_date = c.stat_date + AND f.providerid = c.providerid AND f.catelogid = c.catelogid + WHERE f.distributor_orgid = ${{org}}$ {date_sql}""" + cost_recs = await sor.sqlExe(sql_cost, ns) + if cost_recs: + total_cost_cny = round(float(cost_recs[0].total_cost_cny or 0), 2) + except Exception: + pass + + gross_profit_cny = round(total_revenue_cny - total_cost_cny, 2) + + return { + 'revenue_by_currency': revenue_by_currency, + 'total_revenue_cny': total_revenue_cny, + 'total_cost_cny': total_cost_cny, + 'gross_profit_cny': gross_profit_cny, + 'total_calls': total_calls, + 'period': period, + } diff --git a/sage_datamart/init.py b/sage_datamart/init.py index 9b74a07..a732b56 100644 --- a/sage_datamart/init.py +++ b/sage_datamart/init.py @@ -428,6 +428,40 @@ async def api_user_apikeys(request): debug("sage_datamart API error: " + str(e)) return [] + +async def api_distributor_financial(request): + """分销商财务统计 API""" + try: + env = request._run_ns + scope, org_filter = await _resolve_view(request) + period = (request._run_ns.params_kw or {}).get('period', 'today') + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_distributor_financial(sor, org_filter, period) + except Exception as e: + debug("sage_datamart API error: " + str(e)) + return {} + + +async def j2_distributor_financial(request, period='today'): + """Jinja2: 分销商财务数据""" + try: + env = request._run_ns + scope, org_filter = await _resolve_view(request) + async with get_sor_context(env, MODULE_NAME) as sor: + return await get_distributor_financial(sor, org_filter, period) + except Exception: + return {'total_revenue_cny': 0, 'total_cost_cny': 0, 'gross_profit_cny': 0, + 'revenue_by_currency': [], 'total_calls': 0, 'period': period} + + +async def j2_is_distributor(request): + """Jinja2: 是否分销商角色""" + try: + scope, _ = await _resolve_view(request) + return scope == DISTRIBUTOR + except Exception: + return False + # ── 监控组件 j2 函数,每段独立 .ui 通过 RefreshWidget 加载 ── async def j2_model_perf_data(request): @@ -544,6 +578,9 @@ def load_sage_datamart(): env.j2_scoped_success_rate = j2_scoped_success_rate env.j2_scoped_fail_count = j2_scoped_fail_count env.api_user_apikeys = api_user_apikeys + env.api_distributor_financial = api_distributor_financial + env.j2_distributor_financial = j2_distributor_financial + env.j2_is_distributor = j2_is_distributor env.j2_model_perf_data = j2_model_perf_data env.j2_currency_data = j2_currency_data env.j2_provider_roi_data = j2_provider_roi_data diff --git a/scripts/load_path.py b/scripts/load_path.py index de3cb26..e5828ac 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -40,6 +40,8 @@ PATHS_LOGINED = [ f"/{MOD}/sect_top_users.ui", f"/{MOD}/sect_top_providers.ui", f"/{MOD}/sect_user_apikeys.ui", + f"/{MOD}/sect_distributor_financial.ui", + f"/{MOD}/sect_distributor_currency.ui", ] def run(role, path): diff --git a/wwwroot/api/distributor_currency.dspy b/wwwroot/api/distributor_currency.dspy new file mode 100644 index 0000000..3093d65 --- /dev/null +++ b/wwwroot/api/distributor_currency.dspy @@ -0,0 +1,6 @@ +# 分销商币种收入排行 +import json +env = request._run_ns +data = await env.api_distributor_financial(request) +currency_data = data.get('revenue_by_currency', []) if data else [] +return json.dumps(currency_data, ensure_ascii=False) diff --git a/wwwroot/api/distributor_financial.dspy b/wwwroot/api/distributor_financial.dspy new file mode 100644 index 0000000..c8c9fd9 --- /dev/null +++ b/wwwroot/api/distributor_financial.dspy @@ -0,0 +1,13 @@ +# 分销商财务统计 +import json +import asyncio + +env = request._run_ns +period = (request._run_ns.params_kw or {}).get('period', 'today') + +data = await env.api_distributor_financial(request) +# Fetch all 4 periods +today_data = data +month_data = await env.api_distributor_financial(request) if period != 'today' else data +# Actually, let's just return the single period +return json.dumps(data, ensure_ascii=False) diff --git a/wwwroot/index.ui b/wwwroot/index.ui index b3e12fc..a71d85e 100644 --- a/wwwroot/index.ui +++ b/wwwroot/index.ui @@ -1,6 +1,7 @@ {% set roles = get_user_roles(get_user()) %} {% set userorgid = get_userorgid() if get_userorgid is defined else '' %} {% set is_admin = userorgid == '0' %} +{% set is_distributor = j2_is_distributor(request) %} { "widgettype": "VBox", "id": "datamart_root", @@ -46,6 +47,16 @@ {"widgettype": "RefreshWidget", "id": "rt_online", "options": {"period_seconds": 30, "url": "{{entire_url('rt_card_online.ui')}}", "width": "34%"}} ] }, +{% else %} +{% if is_distributor %} + { + "widgettype": "VBox", + "options": {"css": "card", "width": "100%", "padding": "16px", "marginBottom": "16px"}, + "subwidgets": [ + {"widgettype": "Title4", "options": {"fontWeight": "600", "otext": "财务概览", "i18n": true, "marginBottom": "8px"}}, + {"widgettype": "RefreshWidget", "options": {"period_seconds": 60, "url": "{{entire_url('sect_distributor_financial.ui')}}", "width": "100%"}} + ] + }, {% else %} { "widgettype": "HBox", @@ -69,6 +80,7 @@ } ] }, +{% endif %} {% endif %} { "widgettype": "HBox", diff --git a/wwwroot/sect_distributor_currency.ui b/wwwroot/sect_distributor_currency.ui new file mode 100644 index 0000000..10aa18c --- /dev/null +++ b/wwwroot/sect_distributor_currency.ui @@ -0,0 +1,9 @@ +{ + "widgettype": "ChartBar", + "options": { + "height": "200px", + "data_url": "/sage_datamart/api/distributor_currency.dspy", + "nameField": "currency", + "valueFields": ["revenue_cny"] + } +} diff --git a/wwwroot/sect_distributor_financial.ui b/wwwroot/sect_distributor_financial.ui new file mode 100644 index 0000000..59e6b43 --- /dev/null +++ b/wwwroot/sect_distributor_financial.ui @@ -0,0 +1,56 @@ +{% set env = request._run_ns %} +{% set scope, orgid = _resolve_view(request) %} +{% set today_data = j2_distributor_financial(request, 'today') %} +{% set month_data = j2_distributor_financial(request, 'month') %} +{% set year_data = j2_distributor_financial(request, 'year') %} +{% set all_data = j2_distributor_financial(request, 'all') %} + +{% macro fmt(val) %}{{'¥' + ('%.2f' % val)}}{% endmacro %} + +{ + "widgettype": "VBox", + "options": {"width": "100%"}, + "subwidgets": [ + {"widgettype": "HBox", "options": {"width": "100%", "gap": "12px", "marginBottom": "12px"}, + "subwidgets": [ + {"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "今天", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}}, + {"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(today_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}}, + {"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(today_data.total_cost_cny))}}, "fontSize": "11px"}}, + {"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(today_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if today_data.gross_profit_cny >= 0 else '#ef4444')}}}} + ] + }, + {"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "本月", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}}, + {"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(month_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}}, + {"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(month_data.total_cost_cny))}}, "fontSize": "11px"}}, + {"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(month_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if month_data.gross_profit_cny >= 0 else '#ef4444')}}}} + ] + }, + {"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "本年", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}}, + {"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(year_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}}, + {"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(year_data.total_cost_cny))}}, "fontSize": "11px"}}, + {"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(year_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if year_data.gross_profit_cny >= 0 else '#ef4444')}}}} + ] + }, + {"widgettype": "VBox", "options": {"css": "card", "padding": "12px", "borderRadius": "6px", "width": "25%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "累计", "fontWeight": "600", "fontSize": "12px", "color": "var(--sage-text-primary)"}}, + {"widgettype": "Text", "options": {"text": "收入: " + {{json.dumps(fmt(all_data.total_revenue_cny))}}, "fontSize": "11px", "marginTop": "4px"}}, + {"widgettype": "Text", "options": {"text": "成本: " + {{json.dumps(fmt(all_data.total_cost_cny))}}, "fontSize": "11px"}}, + {"widgettype": "Text", "options": {"text": "毛利: " + {{json.dumps(fmt(all_data.gross_profit_cny))}}, "fontSize": "11px", "color": {{json.dumps('#10b981' if all_data.gross_profit_cny >= 0 else '#ef4444')}}}} + ] + } + ] + }, + {"widgettype": "RefreshWidget", "options": { + "period_seconds": 60, + "url": "/sage_datamart/sect_distributor_currency.ui", + "width": "100%" + }} + ] +}