feat: 分销商财务仪表盘(收入/成本/毛利×币种,今天/本月/本年/累计)

This commit is contained in:
yumoqing 2026-07-24 13:08:31 +08:00
parent 59072ef035
commit 25cfa19a48
8 changed files with 208 additions and 0 deletions

View File

@ -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,
}

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -0,0 +1,9 @@
{
"widgettype": "ChartBar",
"options": {
"height": "200px",
"data_url": "/sage_datamart/api/distributor_currency.dspy",
"nameField": "currency",
"valueFields": ["revenue_cny"]
}
}

View File

@ -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%"
}}
]
}