From 79d6cedaf1abe3634fe34734cfd4e51f266a3a65 Mon Sep 17 00:00:00 2001 From: 24024 <240241002@qq.com> Date: Fri, 23 Jan 2026 00:15:38 +0800 Subject: [PATCH] feat: Add dashboard statistics endpoint and initial frontend application for managing companies and reports. --- backend/app/__pycache__/main.cpython-312.pyc | Bin 3055 -> 3143 bytes backend/app/main.py | 3 +- .../routers/__pycache__/stats.cpython-312.pyc | Bin 0 -> 2834 bytes backend/app/routers/stats.py | 42 ++++++++++++++ backend/logs/app_2026-01-23.log | 6 ++ frontend/js/app.js | 54 ++++++++---------- 6 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 backend/app/routers/__pycache__/stats.cpython-312.pyc create mode 100644 backend/app/routers/stats.py diff --git a/backend/app/__pycache__/main.cpython-312.pyc b/backend/app/__pycache__/main.cpython-312.pyc index c3dc46f22eb6f369068d13408bd8a0e9097e06d6..41e088f75b4927fe8cbc683035414b84adfd2060 100644 GIT binary patch delta 744 zcmaDaeq4h0G%qg~0}v?t6lE4nIGryamWi1kQau@Gc#t{O`gD{E)JwI-Ie7$IeA`A?E_EhD`2F&uR;#ode zREWXMU}ONA;Xk>AS(8y?@;v4SM%KylESl`MSc^*%ONuvpu>4_U5q0Oi{`60(jrXt?S?VKt)U}FUm3kvi~OEPnc^>Xsl(^HFz_<^d6BtV2P zh|mBL0+X+EmT7>@DbfTHS|9>s(=QI2-29Z%oK(9aXCN08JjE@OOS#lo+^#aXO`gxS gOXvmz^K}NHiwr^&OsCsSw7J5dFgcQY5(~%(0J775hX4Qo delta 578 zcmX>u@m`$wG%qg~0}%Z9U6AQMkynzjY@&L;7b8OoV+vP}aIQ#{2$;>CBbqA~CC12* z%9+BG%ACfO!rQ{K8l({jqQsdPQkhe^vkV}7WOCv;aY6Ale5;uuf>BbbQd#Dcc^LI2 zrLruM6++ml{8?6$Ke7r>wrAqPp+bP53PFSlW}q#TA25nC!c656wZXJC%YJeKlPIIj zWFIC?BvPGGaq=f7Wl5z}#w={Q+$Y~-l3^>71$xPIasaa=qtfIoW_f0@EU(EuKw2ux zck()BO-A*}H<%k3StduaXm0Lg`NPP_H@S_?P?Q(wj$5q7C5c5Pw^)ix@(VN>i})v> zVKZSApUlXf*0M}`DlK=n! diff --git a/backend/app/main.py b/backend/app/main.py index 5389069..0c9037e 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,7 +9,7 @@ from contextlib import asynccontextmanager from app.config import settings from app.database import init_db -from app.routers import companies, reports, analysis, scheduler +from app.routers import companies, reports, analysis, scheduler, stats from app.services.scheduler_service import scheduler_service from app.utils.logger import logger @@ -55,6 +55,7 @@ app.include_router(companies.router) app.include_router(reports.router) app.include_router(analysis.router) app.include_router(scheduler.router) +app.include_router(stats.router) # 挂载静态文件 (前端) # 确保frontend目录存在 diff --git a/backend/app/routers/__pycache__/stats.cpython-312.pyc b/backend/app/routers/__pycache__/stats.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..582021aeb8b567995ab133aa944844ac4dab32e0 GIT binary patch literal 2834 zcmcguZEO@p7@pbN-Mijh4`{FTdI&AF2ORVx8s(!_^h!15tC0^)I4YOD-D$h$?%grF zg&vgT{D_)}Ux^9?43hAp2=Wn0Ao9zP5EJ>)Wl6SdLcl19|5_kX`Q^-Bx7Q%XA0$q) z^Stjn@6Nn4^UgE#Q)Q(ekaA}&mb-ZXexZt6*h%5;H$>P6Bp^Wrs4xjtCdojmGm1xL zldL0q6i(%nyy{JQRbSHQ*jS}P^(Xy~%qamim<&2HuT-j`WJsNnoT1K4&V+yg2ufbb z*AGur9A`@7CTF2pnV1_)872R5SklN}3<|nhACv-!?PH|iTio$U_hfaUG9LP!YH`M{ zNW8RqV{TAK*!FgzJjzO19NNKzR>+E*kfzDGY}{k>8d8v`+ic%pR>#f z6C|SZ{YXzqY2xo)l~eOVwqWxcQ9g%tJCw)@N_C8p2pKHlDY2a!lbQktPe3K-?ZZg7W%s$WC90&Hm zM~s3lic=mb3g-WeA~VB{xr};w9y7~USCW|576I^|Yw4M`cT@eje?97`nQAwhGU_$1 zJT3lO*H%V11{X7R6WR>s7{sKw^nE$%F^X zLGZbUv^-?xGfN)y1wi85xl8;Z519)jkx)|h4l`4dXG$W}dWhWwmVuAp-m6Z1z^PX( z1AF&><=F}26@zrgZhvs@=7*pCdg064M@D`-xPLlR^=$r~GR-beLrcRhUu;WTOl^xv zg#Q>>vN>JW6-3BVAmmEmbVu=@1Vs0?2+d=HGlB+A^PfbtBSxxRA z(Brj)hDAXUFs8JHDf8K!(-96(15IR1&xg(J96%V^0a;7AecHYpmJm**3z!m|?H|n3 zz@(I*+nge+vTl2Gd7aR`hAXIpcQRDli-wR$E(p7-G`G)E!-V zqUqG)V~fwea&F6+E#~6q%*N-u5?eR*Pm#GG`;J1{Hhm@gP|B$%6M*T7h~aP=A!S8lE&7 zo;)oX4ee&&87t6b1iF6Md})ETW~;GgtNB8&8F?evjSNokiC-IeGia} zXBG1Yg|*B?3GM=rhwB&u!SzhBYpl9vU;D@H`z5ow(W-1LzBm?+THzKW++v2?itBIG z)D?TKdc!xW>aD8zM%DbWNX&|~7?Bn$vfPL)zZ}_ie!bPP!RXjvb!;;_wv9fP8cn4~ z#k?i%FvK0B>7AoBLu282R=C*+H(TMSjquZ_GiEq3;bo)#cSGYL5M6k=Zpp~Ju}IV9 zNPGm2MHgL;wu~eQPE>F~ejIQdKfVOibiu#Kx`sO)sO=-*1EHq-+FG)SZ#>r>gWp2f z9c8~|5)4tkV`29k_B$>ypD5oigxwA7_lr8Ia?uaF8`+D2&Y48{aSo()gHt!sx+fmC zy?p{nlDv%RD*@A2o?WYLR|G{IKx&~C4e6}{eGJ863iknJO_l=bfWI>=vx z3R;Pm5tR~<_A;r)JrKg{V8J!eb{)hG5WfZ%-URj6!BPV({kfuQfAdIowB^|=6)TJU s4d{I*`g-);btbI4`o!|{wO`jyfVq(WqWKPmyB@$qCm15(rKIP-0jCRj#sB~S literal 0 HcmV?d00001 diff --git a/backend/app/routers/stats.py b/backend/app/routers/stats.py new file mode 100644 index 0000000..af29a6e --- /dev/null +++ b/backend/app/routers/stats.py @@ -0,0 +1,42 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, func, desc +from sqlalchemy.orm import selectinload + +from app.database import get_db +from app.models import Company, Report, AnalysisResult + +router = APIRouter(prefix="/api/stats", tags=["统计数据"]) + +@router.get("/dashboard") +async def get_dashboard_stats(db: AsyncSession = Depends(get_db)): + """获取仪表盘统计数据""" + + # 统计公司数 + company_count = await db.scalar(select(func.count(Company.id))) + + # 统计报告数 + report_count = await db.scalar(select(func.count(Report.id))) + + # 统计已分析数 + analyzed_count = await db.scalar(select(func.count(Report.id)).where(Report.is_analyzed == True)) + + # 获取最新的一条分析完成的报告 + stmt = select(Report).where(Report.is_analyzed == True).order_by(desc(Report.updated_at)).limit(1).options(selectinload(Report.company)) + result = await db.execute(stmt) + latest_report = result.scalar_one_or_none() + + latest_insight = None + if latest_report: + latest_insight = { + "id": latest_report.id, + "title": f"{latest_report.company.short_name or latest_report.company.company_name} - {latest_report.title}", + "date": latest_report.updated_at.strftime("%Y/%m/%d") + } + + return { + "company_count": company_count or 0, + "report_count": report_count or 0, + "analyzed_count": analyzed_count or 0, + "latest_insight": latest_insight + } diff --git a/backend/logs/app_2026-01-23.log b/backend/logs/app_2026-01-23.log index 98f35ef..1311ac6 100644 --- a/backend/logs/app_2026-01-23.log +++ b/backend/logs/app_2026-01-23.log @@ -1 +1,7 @@ 2026-01-23 00:00:32 | WARNING | app.services.ai_analyzer:call_ai:77 - AI调用异常: Server disconnected without sending a response.,2秒后重试... +2026-01-23 00:11:08 | INFO | app.main:lifespan:34 - 应用正在关闭... +2026-01-23 00:11:08 | INFO | app.services.scheduler_service:stop:49 - 定时任务调度器已停止 +2026-01-23 00:11:25 | INFO | app.main:lifespan:21 - 正在启动 眼镜行业情报分析系统... +2026-01-23 00:11:39 | INFO | app.main:lifespan:21 - 正在启动 眼镜行业情报分析系统... +2026-01-23 00:11:39 | INFO | app.main:lifespan:25 - 数据库已初始化 +2026-01-23 00:11:39 | INFO | app.services.scheduler_service:start:42 - 定时任务调度器已启动,间隔: 24小时 diff --git a/frontend/js/app.js b/frontend/js/app.js index 3253a22..446b0a4 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -252,44 +252,40 @@ const app = { // 获取仪表盘数据 async fetchDashboard() { try { - // 获取统计数据 - const [companies, reports, logs] = await Promise.all([ - fetch(`${API_BASE}/companies`).then(r => r.json()), - fetch(`${API_BASE}/reports?page_size=5`).then(r => r.json()), + // 获取统计数据和日志 + const [stats, logs] = await Promise.all([ + fetch(`${API_BASE}/stats/dashboard`).then(r => r.json()), fetch(`${API_BASE}/scheduler/logs`).then(r => r.json()) ]); // 更新计数 - document.getElementById('stat-companies').textContent = companies.length; - document.getElementById('stat-reports').textContent = reports.length; // 实际应该请求总数API - - // 简单估算已分析数(实际应从API获取) - const analyzedCount = reports.filter(r => r.is_analyzed).length; - document.getElementById('stat-analyzed').textContent = analyzedCount; + document.getElementById('stat-companies').textContent = stats.company_count; + document.getElementById('stat-reports').textContent = stats.report_count; + document.getElementById('stat-analyzed').textContent = stats.analyzed_count; // 渲染最新洞察 const insightsContainer = document.getElementById('recent-insights-list'); insightsContainer.innerHTML = ''; - reports.slice(0, 5).forEach(report => { - if (report.is_analyzed) { - const el = document.createElement('div'); - el.className = 'insight-item card'; - el.style.marginBottom = '10px'; - el.style.padding = '12px'; - el.innerHTML = ` -
${report.company_name} - ${report.title}
-
- AI分析已完成 • ${new Date(report.created_at).toLocaleDateString()} -
- - `; - insightsContainer.appendChild(el); - } - }); - - if (insightsContainer.children.length === 0) { - insightsContainer.innerHTML = '
暂无分析结果
'; + if (stats.latest_insight) { + const report = stats.latest_insight; + const el = document.createElement('div'); + el.className = 'insight-item card'; + el.style.marginBottom = '10px'; + el.style.padding = '16px'; + el.style.borderLeft = '4px solid #3b82f6'; + el.innerHTML = ` +
${report.title}
+
+ AI分析已完成 • ${report.date} +
+ + `; + insightsContainer.appendChild(el); + } else { + insightsContainer.innerHTML = '
暂无分析结果
'; } // 渲染日志