Files
tech-blog/app/services/visit_service.py
hz4th_coder e689fed71e 初始化技术博客项目
功能特性:
- 前端文章展示 (Markdown渲染、代码高亮)
- 后台管理系统 (文章、分类、标签、作者管理)
- 图片上传 (点击、拖拽、粘贴)
- 网站设置 (Logo、底部宣传图片上传)
- 访问统计 (IP记录、趋势图表)
- 参考来源、附件支持
- 单端口部署 (16012)

技术栈:
- Flask 3.0 + SQLAlchemy
- SQLite数据库
- marked.js + highlight.js (Markdown渲染)
- Chart.js (统计图表)
2026-06-04 23:12:34 +08:00

162 lines
5.4 KiB
Python

"""访问统计服务"""
from datetime import datetime, date, timedelta
from app import db
from app.models import VisitLog, DailyStats, Article
class VisitService:
"""访问统计业务逻辑"""
@staticmethod
def log_visit(request, article_id=None):
"""记录访问日志"""
from flask import request as flask_request
visit = VisitLog(
ip_address=VisitService.get_client_ip(flask_request),
user_agent=flask_request.user_agent.string if flask_request.user_agent else None,
referer=flask_request.referrer,
path=flask_request.path,
method=flask_request.method,
article_id=article_id
)
db.session.add(visit)
db.session.commit()
# 更新每日统计
VisitService.update_daily_stats()
return visit
@staticmethod
def get_client_ip(request):
"""获取客户端真实IP"""
# 检查代理头
if request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
elif request.headers.get('X-Real-IP'):
return request.headers.get('X-Real-IP')
else:
return request.remote_addr or '0.0.0.0'
@staticmethod
def update_daily_stats():
"""更新每日统计"""
today = date.today()
stats = DailyStats.query.filter_by(date=today).first()
if not stats:
stats = DailyStats(date=today)
db.session.add(stats)
# 计算今日数据
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
stats.total_visits = VisitLog.query.filter(
VisitLog.visited_at >= today_start,
VisitLog.visited_at <= today_end
).count()
stats.unique_ips = db.session.query(
db.func.count(db.func.distinct(VisitLog.ip_address))
).filter(
VisitLog.visited_at >= today_start,
VisitLog.visited_at <= today_end
).scalar()
stats.article_views = VisitLog.query.filter(
VisitLog.visited_at >= today_start,
VisitLog.visited_at <= today_end,
VisitLog.article_id.isnot(None)
).count()
# 找出今日最热门文章
top_article_result = db.session.query(
VisitLog.article_id,
db.func.count(VisitLog.id).label('views')
).filter(
VisitLog.visited_at >= today_start,
VisitLog.visited_at <= today_end,
VisitLog.article_id.isnot(None)
).group_by(VisitLog.article_id).order_by(db.desc('views')).first()
if top_article_result:
stats.top_article_id = top_article_result[0]
stats.top_article_views = top_article_result[1]
db.session.commit()
return stats
@staticmethod
def get_stats_summary():
"""获取统计摘要"""
return {
'total_visits': VisitLog.get_total_count(),
'today_visits': VisitLog.get_today_count(),
'unique_ips': VisitLog.get_unique_ip_count(),
'daily_stats': DailyStats.get_stats_list(30)
}
@staticmethod
def get_visit_list(page=1, per_page=50, path_filter=None):
"""获取访问日志列表"""
query = VisitLog.query
if path_filter:
query = query.filter(VisitLog.path.like(f'%{path_filter}%'))
return query.order_by(VisitLog.visited_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
@staticmethod
def get_top_ips(limit=10):
"""获取访问最多的IP"""
from sqlalchemy import func
result = db.session.query(
VisitLog.ip_address,
func.count(VisitLog.id).label('visit_count'),
func.min(VisitLog.visited_at).label('first_visit'),
func.max(VisitLog.visited_at).label('last_visit')
).group_by(VisitLog.ip_address).order_by(db.desc('visit_count')).limit(limit).all()
return result
@staticmethod
def get_top_articles(days=7, limit=10):
"""获取热门文章"""
start_date = date.today() - timedelta(days=days)
start_datetime = datetime.combine(start_date, datetime.min.time())
result = db.session.query(
Article,
db.func.count(VisitLog.id).label('views')
).join(VisitLog).filter(
VisitLog.visited_at >= start_datetime,
VisitLog.article_id.isnot(None)
).group_by(Article.id).order_by(db.desc('views')).limit(limit).all()
return result
@staticmethod
def get_top_paths(limit=20):
"""获取访问最多的路径"""
from sqlalchemy import func
result = db.session.query(
VisitLog.path,
func.count(VisitLog.id).label('visit_count')
).group_by(VisitLog.path).order_by(db.desc('visit_count')).limit(limit).all()
return result
@staticmethod
def clear_old_logs(days=90):
"""清理旧日志"""
cutoff_date = datetime.now() - timedelta(days=days)
deleted = VisitLog.query.filter(VisitLog.visited_at < cutoff_date).delete()
db.session.commit()
return deleted