feat: 客户dashboard新增按用户统计(今日/本月+总计)

load_dashboard.py:
- get_customer_users_today: 今日各用户调用次数+金额,含总计行
- get_customer_users_month: 本月各用户调用次数+金额,含总计行

customer_usage.ui: 底部嵌入 customer_users_table.ui
customer_users_table.ui: 左右双栏 Tabular(今日|本月)
api/: customer_users_today.dspy + customer_users_month.dspy
scripts/load_path.py: +3条路径注册
This commit is contained in:
yumoqing 2026-07-02 14:06:15 +08:00
parent dfad387358
commit 4028d00584
6 changed files with 648 additions and 412 deletions

View File

@ -586,6 +586,99 @@ async def get_customer_daily_trend(request):
return result return result
async def get_customer_users_today(request):
"""获取当前客户组织今日各用户使用统计(含总计行)"""
env = request._run_ns
org_id = await _get_org_id(request)
today = env.curDateString()
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
a.userid,
COALESCE(b.nick_name, b.username) as user_name,
COUNT(*) as call_cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN users b ON a.userid = b.id
WHERE a.use_date = ${today}$
AND a.userorgid = ${org_id}$
GROUP BY a.userid, b.nick_name, b.username
ORDER BY call_cnt DESC
"""
recs = await sor.sqlExe(sql, {'today': today, 'org_id': org_id})
result = []
total_calls = 0
total_amount = 0.0
for r in recs:
c = int(r.get('call_cnt', 0))
a = round(float(r.get('total_amount', 0)), 4)
total_calls += c
total_amount += a
result.append({
'user_name': r.get('user_name', 'Unknown'),
'call_cnt': c,
'total_amount': a
})
# 追加总计行
result.append({
'user_name': '【合计】',
'call_cnt': total_calls,
'total_amount': round(total_amount, 4)
})
return result
async def get_customer_users_month(request):
"""获取当前客户组织当月各用户使用统计(含总计行)"""
env = request._run_ns
org_id = await _get_org_id(request)
now = datetime.now()
month_start = now.strftime('%Y-%m-01')
if now.month == 12:
month_end = f'{now.year + 1}-01-01'
else:
month_end = f'{now.year}-{now.month + 1:02d}-01'
async with get_sor_context(env, 'sage') as sor:
sql = """
SELECT
a.userid,
COALESCE(b.nick_name, b.username) as user_name,
COUNT(*) as call_cnt,
COALESCE(SUM(a.amount), 0) as total_amount
FROM llmusage a
LEFT JOIN users b ON a.userid = b.id
WHERE a.use_date >= ${month_start}$
AND a.use_date < ${month_end}$
AND a.userorgid = ${org_id}$
GROUP BY a.userid, b.nick_name, b.username
ORDER BY call_cnt DESC
"""
recs = await sor.sqlExe(sql, {
'month_start': month_start,
'month_end': month_end,
'org_id': org_id
})
result = []
total_calls = 0
total_amount = 0.0
for r in recs:
c = int(r.get('call_cnt', 0))
a = round(float(r.get('total_amount', 0)), 4)
total_calls += c
total_amount += a
result.append({
'user_name': r.get('user_name', 'Unknown'),
'call_cnt': c,
'total_amount': a
})
result.append({
'user_name': '【合计】',
'call_cnt': total_calls,
'total_amount': round(total_amount, 4)
})
return result
def load_dashboard(): def load_dashboard():
"""Register dashboard functions on ServerEnv""" """Register dashboard functions on ServerEnv"""
g = ServerEnv() g = ServerEnv()
@ -613,3 +706,5 @@ def load_dashboard():
g.get_customer_daily_summary = get_customer_daily_summary g.get_customer_daily_summary = get_customer_daily_summary
g.get_customer_month_summary = get_customer_month_summary g.get_customer_month_summary = get_customer_month_summary
g.get_customer_daily_trend = get_customer_daily_trend g.get_customer_daily_trend = get_customer_daily_trend
g.get_customer_users_today = get_customer_users_today
g.get_customer_users_month = get_customer_users_month

View File

@ -79,6 +79,7 @@ paths = [
("/dashboard_for_sage/customer_daily_chart.ui", "logined"), ("/dashboard_for_sage/customer_daily_chart.ui", "logined"),
("/dashboard_for_sage/customer_monthly_chart.ui", "logined"), ("/dashboard_for_sage/customer_monthly_chart.ui", "logined"),
("/dashboard_for_sage/customer_daily_trend.ui", "logined"), ("/dashboard_for_sage/customer_daily_trend.ui", "logined"),
("/dashboard_for_sage/customer_users_table.ui", "logined"),
# API endpoints # API endpoints
("/dashboard_for_sage/api/top_models.dspy", "logined"), ("/dashboard_for_sage/api/top_models.dspy", "logined"),
@ -87,6 +88,8 @@ paths = [
("/dashboard_for_sage/api/customer_daily_models.dspy", "logined"), ("/dashboard_for_sage/api/customer_daily_models.dspy", "logined"),
("/dashboard_for_sage/api/customer_monthly_models.dspy", "logined"), ("/dashboard_for_sage/api/customer_monthly_models.dspy", "logined"),
("/dashboard_for_sage/api/customer_daily_trend.dspy", "logined"), ("/dashboard_for_sage/api/customer_daily_trend.dspy", "logined"),
("/dashboard_for_sage/api/customer_users_today.dspy", "logined"),
("/dashboard_for_sage/api/customer_users_month.dspy", "logined"),
] ]

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Customer per-user month usage API"""
import json
users = await get_customer_users_month(request)
return json.dumps(users, ensure_ascii=False, default=str)

View File

@ -0,0 +1,6 @@
# coding=utf-8
"""Customer per-user today usage API"""
import json
users = await get_customer_users_today(request)
return json.dumps(users, ensure_ascii=False, default=str)

View File

@ -455,6 +455,12 @@
] ]
} }
] ]
},
{
"widgettype": "urlwidget",
"options": {
"url": "{{entire_url('customer_users_table.ui')}}"
}
} }
] ]
} }

View File

@ -0,0 +1,120 @@
{
"widgettype": "VBox",
"options": {
"css": "card",
"width": "100%",
"borderRadius": "12px",
"padding": "20px",
"marginBottom": "20px"
},
"subwidgets": [
{
"widgettype": "HBox",
"options": {
"width": "100%",
"alignItems": "center",
"marginBottom": "16px"
},
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"fontWeight": "600",
"otext": "按用户统计",
"i18n": true
}
},
{
"widgettype": "Filler"
}
]
},
{
"widgettype": "HBox",
"options": {
"width": "100%",
"gap": "20px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"width": "50%"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"otext": "今日",
"i18n": true,
"fontSize": "14px",
"fontWeight": "600",
"marginBottom": "8px"
}
},
{
"widgettype": "Tabular",
"options": {
"data_url": "{{entire_url('api/customer_users_today.dspy')}}",
"data_method": "GET",
"width": "100%",
"height": "auto",
"row_options": {
"browserfields": {
"exclouded": [],
"cwidth": {}
},
"fields": [
{"name": "user_name", "title": "用户", "type": "str", "uitype": "str", "label": "用户", "cwidth": 20},
{"name": "call_cnt", "title": "调用次数", "type": "int", "uitype": "int", "label": "调用次数", "cwidth": 12},
{"name": "total_amount", "title": "金额", "type": "float", "uitype": "float", "label": "金额", "cwidth": 12}
]
},
"page_rows": 50
}
}
]
},
{
"widgettype": "VBox",
"options": {
"width": "50%"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"otext": "本月",
"i18n": true,
"fontSize": "14px",
"fontWeight": "600",
"marginBottom": "8px"
}
},
{
"widgettype": "Tabular",
"options": {
"data_url": "{{entire_url('api/customer_users_month.dspy')}}",
"data_method": "GET",
"width": "100%",
"height": "auto",
"row_options": {
"browserfields": {
"exclouded": [],
"cwidth": {}
},
"fields": [
{"name": "user_name", "title": "用户", "type": "str", "uitype": "str", "label": "用户", "cwidth": 20},
{"name": "call_cnt", "title": "调用次数", "type": "int", "uitype": "int", "label": "调用次数", "cwidth": 12},
{"name": "total_amount", "title": "金额", "type": "float", "uitype": "float", "label": "金额", "cwidth": 12}
]
},
"page_rows": 50
}
}
]
}
]
}
]
}